This commit is contained in:
StressOzz authored and GitHub committed 2026-10-25 15:20:57 +05:00
1 parent a28079af6f
commit ce90def613
1 file changed
+225 -110
+225 -110
View File
@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
read -r _ _ ZM_NEW_VER <<'ZM_VERSION_EOF' read -r _ _ ZM_NEW_VER <<'ZM_VERSION_EOF'
# Version: 4.53 # Version: 4.54
ZM_VERSION_EOF ZM_VERSION_EOF
set -e set -e
@@ -1549,7 +1549,6 @@ zm_watch() {
_fk_listsvia_patch _fk_listsvia_patch
_fk_ghdl_patch _fk_ghdl_patch
_fk_b4_watch _fk_b4_watch
_fk_subfmt_watch
fi fi
_st_colo_guard _st_colo_guard
_st_vpn_watch _st_vpn_watch
@@ -1559,6 +1558,7 @@ zm_watch() {
_dn_watch _dn_watch
_zo_doh_follow _zo_doh_follow
_doh_boot_fill _doh_boot_fill
_ft_steer_sync
return 0 return 0
} }
@@ -3556,6 +3556,55 @@ _zapret_v6_set() {
fi fi
} }
# Строка выгрузки потоков в шаблоне firewall4 (chain forward):
# meta l4proto { tcp, udp } [ct mark and 0x0ff00000 == 0x0 ][ct original packets ge 30 ]flow offload @ft;
# «ct original packets ge 30» — FIX для Zapret: первые 30 пакетов соединения идут обычным путём, их видит
# Zapret. «ct mark and 0x0ff00000 == 0x0» — соединения, которые Steer отправил в выход (VPN, интерфейс,
# Zapret), не выгружаются вовсе: Steer ставит метку соединения в этих битах. С включённым Flow Offloading
# (особенно аппаратным) выгруженное соединение уходит в туннель Steer мимо обычного пути — по кабелю
# трафик секции VPN переставал идти, по Wi‑Fi (его аппаратно не выгружают) работал. Остальной трафик
# выгружается, как раньше, — скорость не теряется
FW4_TPL="/usr/share/firewall4/templates/ruleset.uc"
# _fw4_offload_set STEER(0|1|keep) ZFIX(0|1|keep) — переписать строку; 0 — изменено, 1 — не менялось, 2 — строки нет
_fw4_offload_set() {
local st="$1" zf="$2" t="$FW4_TPL.zm.$$"
[ -f "$FW4_TPL" ] || return 2
awk -v st="$st" -v zf="$zf" '
BEGIN { re = "^[ \t]*meta l4proto [{] tcp, udp [}] (ct mark and 0x0ff00000 == 0x0 )?(ct original packets ge 30 )?flow offload @ft;$" }
!done && $0 ~ re {
ind = $0; sub(/meta l4proto.*$/, "", ind)
hs = index($0, "ct mark and 0x0ff00000 == 0x0 ") > 0; hz = index($0, "ct original packets ge 30 ") > 0
if (st == "1") hs = 1; else if (st == "0") hs = 0
if (zf == "1") hz = 1; else if (zf == "0") hz = 0
n = ind "meta l4proto { tcp, udp } " (hs ? "ct mark and 0x0ff00000 == 0x0 " : "") (hz ? "ct original packets ge 30 " : "") "flow offload @ft;"
if (n != $0) ch = 1
print n; done = 1; next
}
{ print }
END { exit done ? (ch ? 0 : 1) : 2 }' "$FW4_TPL" > "$t"
case $? in
0) cat "$t" > "$FW4_TPL"; rm -f "$t"; return 0 ;;
1) rm -f "$t"; return 1 ;;
*) rm -f "$t"; return 2 ;;
esac
}
_fw4_offload_on() { [ "$(uci -q get firewall.@defaults[0].flow_offloading)" = 1 ] || [ "$(uci -q get firewall.@defaults[0].flow_offloading_hw)" = 1 ]; }
_ft_steer_applied() { grep -q 'ct mark and 0x0ff00000 == 0x0 .*flow offload @ft;' "$FW4_TPL" 2>/dev/null; }
# Steer стоит — его соединения из выгрузки исключены; Steer удалён — исключение убираем
_ft_steer_sync() {
local want=0
[ -f "$FW4_TPL" ] || return 0
stl_present && want=1
if [ "$want" = 1 ]; then _ft_steer_applied && return 0
else _ft_steer_applied || return 0; fi
_fw4_offload_set "$want" keep || return 0
logger -t zapret-manager "Flow Offloading: соединения Steer $([ "$want" = 1 ] && echo "исключены из выгрузки потоков" || echo "снова выгружаются — Steer удалён")"
_fw4_offload_on && /etc/init.d/firewall reload >/dev/null 2>&1
return 0
}
_flow_offloading_fix_applied() { _flow_offloading_fix_applied() {
grep -q 'ct original packets ge 30 flow offload @ft;' /usr/share/firewall4/templates/ruleset.uc 2>/dev/null \ grep -q 'ct original packets ge 30 flow offload @ft;' /usr/share/firewall4/templates/ruleset.uc 2>/dev/null \
&& echo true || echo false && echo true || echo false
@@ -3685,12 +3734,12 @@ system_toggle_flow_offloading_fix() {
local template="/usr/share/firewall4/templates/ruleset.uc" local template="/usr/share/firewall4/templates/ruleset.uc"
[ -f "$template" ] || { echo '{"error":"шаблон firewall4 не найден"}'; return 1; } [ -f "$template" ] || { echo '{"error":"шаблон firewall4 не найден"}'; return 1; }
if grep -q 'ct original packets ge 30 flow offload @ft;' "$template"; then if grep -q 'ct original packets ge 30 flow offload @ft;' "$template"; then
sed -i 's/meta l4proto { tcp, udp } ct original packets ge 30 flow offload @ft;/meta l4proto { tcp, udp } flow offload @ft;/' "$template" _fw4_offload_set keep 0
fw4 restart >/dev/null 2>&1 fw4 restart >/dev/null 2>&1
printf '{"ok":true,"flow_offloading_fix":false}\n' printf '{"ok":true,"flow_offloading_fix":false}\n'
return return
fi fi
sed -i 's/meta l4proto { tcp, udp } flow offload @ft;/meta l4proto { tcp, udp } ct original packets ge 30 flow offload @ft;/' "$template" _fw4_offload_set keep 1
if ! grep -q 'ct original packets ge 30 flow offload @ft;' "$template"; then if ! grep -q 'ct original packets ge 30 flow offload @ft;' "$template"; then
echo '{"error":"в шаблоне firewall4 нет ожидаемой строки flow offload — исправление не применилось, ничего не изменено"}' echo '{"error":"в шаблоне firewall4 нет ожидаемой строки flow offload — исправление не применилось, ничего не изменено"}'
return 1 return 1
@@ -3757,6 +3806,9 @@ _zm_junk_raw() {
_zm_aged "$f" "$age" && echo "$f" _zm_aged "$f" "$age" && echo "$f"
done done
[ "$1" = install ] || { [ -e /tmp/zm_update_install.log ] && echo /tmp/zm_update_install.log; } [ "$1" = install ] || { [ -e /tmp/zm_update_install.log ] && echo /tmp/zm_update_install.log; }
# недописанная программа TG WS Proxy от неудачной установки: файла нет у службы и версии
[ -f "$TG_BIN_RS" ] && [ ! -e "$TG_INIT_RS" ] && [ ! -s "$TG_VER_RS_FILE" ] && echo "$TG_BIN_RS"
[ -f "$TG_BIN_GO" ] && [ ! -e "$TG_INIT_GO" ] && [ ! -s "$TG_VER_GO_FILE" ] && echo "$TG_BIN_GO"
[ -e "$ZM_UP_FILE" ] && echo "$ZM_UP_FILE" [ -e "$ZM_UP_FILE" ] && echo "$ZM_UP_FILE"
for f in /tmp/zm-rpc.*; do _zm_tmp_live "$f" && continue; _zm_aged "$f" 10 && echo "$f"; done for f in /tmp/zm-rpc.*; do _zm_tmp_live "$f" && continue; _zm_aged "$f" 10 && echo "$f"; done
if ! _job_running strategy_test; then if ! _job_running strategy_test; then
@@ -3876,6 +3928,28 @@ zm_refs_heal() {
_zm_mem_kb() { awk '/^MemAvailable:/ { print $2; f = 1; exit } /^MemFree:/ { m = $2 } END { if (!f) print m + 0 }' /proc/meminfo 2>/dev/null; } _zm_mem_kb() { awk '/^MemAvailable:/ { print $2; f = 1; exit } /^MemFree:/ { m = $2 } END { if (!f) print m + 0 }' /proc/meminfo 2>/dev/null; }
_zm_df_kb() { df -kP "$1" 2>/dev/null | awk 'NR == 2 { print $4 + 0; f = 1 } END { if (!f) print 0 }'; } _zm_df_kb() { df -kP "$1" 2>/dev/null | awk 'NR == 2 { print $4 + 0; f = 1 } END { if (!f) print 0 }'; }
# Положить скачанную программу на флеш: сначала проверяем место (с запасом 128 КБ; место прежней версии
# освободится), при сбое записи убираем недописанный файл. Раньше программа, не влезшая на флеш, оставалась
# недописанной в /usr/bin и занимала место — его не возвращала и «Очистка», и потом не хватало места
# уже другим компонентам. _zm_bin_put ИСТОЧНИК ЦЕЛЬ
_zm_bin_put() {
local need have old=0
need=$(( ($(wc -c < "$1" 2>/dev/null || echo 0) + 1023) / 1024 ))
[ -f "$2" ] && old=$(( ($(wc -c < "$2" 2>/dev/null || echo 0) + 1023) / 1024 ))
have="$(_zm_df_kb "${2%/*}")"
if [ "$have" -gt 0 ] && [ $((have + old)) -lt $((need + 128)) ]; then
echo "ОШИБКА: не хватает места на флеше: программе нужно $(( (need + 1023) / 1024 )) МБ, свободно $(( (have + old) / 1024 )) МБ. Освободите место (удалите ненужные компоненты в разделе «Система») и попробуйте снова"
return 1
fi
rm -f "$2"
if ! cp "$1" "$2" 2>/dev/null || ! chmod +x "$2"; then
rm -f "$2"
echo "ОШИБКА: программа не записалась на флеш — похоже, кончилось место. Недописанный файл удалён"
return 1
fi
return 0
}
_zm_usage_json() { _zm_usage_json() {
local list="$JOBS_DIR/cleanup.$$" jk=0 jn=0 local list="$JOBS_DIR/cleanup.$$" jk=0 jn=0
mkdir -p "$JOBS_DIR" mkdir -p "$JOBS_DIR"
@@ -3916,6 +3990,7 @@ zm_cleanup() {
/tmp/zm[-_]*|/tmp/ytb-*|/tmp/bytetube-test/?*|/tmp/netshift-sbext.*|/tmp/netshift-sblite.*|/tmp/mihomo.gz|/tmp/zashboard|/tmp/zashboard.zip|/tmp/metacubexd|/tmp/metacubexd.tgz|/tmp/tg-ws-proxy.ipk|/tmp/tg-ws-proxy.apk) ;; /tmp/zm[-_]*|/tmp/ytb-*|/tmp/bytetube-test/?*|/tmp/netshift-sbext.*|/tmp/netshift-sblite.*|/tmp/mihomo.gz|/tmp/zashboard|/tmp/zashboard.zip|/tmp/metacubexd|/tmp/metacubexd.tgz|/tmp/tg-ws-proxy.ipk|/tmp/tg-ws-proxy.apk) ;;
/tmp/zapret-manager-luci/?*|/opt/zapret-manager-luci/?*|/etc/zm-steer/?*|/etc/hosts.zmtmp|/etc/hosts.zmdrop) ;; /tmp/zapret-manager-luci/?*|/opt/zapret-manager-luci/?*|/etc/zm-steer/?*|/etc/hosts.zmtmp|/etc/hosts.zmdrop) ;;
/www/luci-static/resources/zapret-manager/?*|/www/luci-static/resources/view/zapret-manager/?*) ;; /www/luci-static/resources/zapret-manager/?*|/www/luci-static/resources/view/zapret-manager/?*) ;;
"$TG_BIN_RS"|"$TG_BIN_GO") ;;
*) continue ;; *) continue ;;
esac esac
case "$f" in */../*|*/..) continue ;; esac case "$f" in */../*|*/..) continue ;; esac
@@ -6190,7 +6265,8 @@ do_tg_install_socks5() {
fi fi
chmod +x "$tmp" chmod +x "$tmp"
[ -x "$TG_INIT_GO" ] && "$TG_INIT_GO" stop >/dev/null 2>&1 [ -x "$TG_INIT_GO" ] && "$TG_INIT_GO" stop >/dev/null 2>&1
mv -f "$tmp" "$TG_BIN_GO" || { rm -f "$tmp"; echo "ОШИБКА: не удалось записать программу"; return 1; } _zm_bin_put "$tmp" "$TG_BIN_GO" || { rm -f "$tmp"; [ -x "$TG_INIT_GO" ] && [ -x "$TG_BIN_GO" ] && "$TG_INIT_GO" start >/dev/null 2>&1; return 1; }
rm -f "$tmp"
if [ ! -f "$TG_INIT_GO" ]; then if [ ! -f "$TG_INIT_GO" ]; then
printf '#!/bin/sh /etc/rc.common\nSTART=99\nUSE_PROCD=1\n\nstart_service() {\n\tprocd_open_instance\n\tprocd_set_param command /usr/bin/tg-ws-proxy-go --host 0.0.0.0 --port 2080 --cf-proxy --cf-proxy-first --cf-balance\n\tprocd_set_param respawn\n\tprocd_close_instance\n}\n' > "$TG_INIT_GO" printf '#!/bin/sh /etc/rc.common\nSTART=99\nUSE_PROCD=1\n\nstart_service() {\n\tprocd_open_instance\n\tprocd_set_param command /usr/bin/tg-ws-proxy-go --host 0.0.0.0 --port 2080 --cf-proxy --cf-proxy-first --cf-balance\n\tprocd_set_param respawn\n\tprocd_close_instance\n}\n' > "$TG_INIT_GO"
chmod +x "$TG_INIT_GO" chmod +x "$TG_INIT_GO"
@@ -6237,9 +6313,7 @@ do_tg_install_rust() {
set -- $(find "$tmp_dir" -type f -name 'tg-ws-proxy*' 2>/dev/null) set -- $(find "$tmp_dir" -type f -name 'tg-ws-proxy*' 2>/dev/null)
[ -n "$1" ] || { echo "ОШИБКА: в архиве нет программы"; rm -rf "$tmp_dir" "$tmp_archive"; return 1; } [ -n "$1" ] || { echo "ОШИБКА: в архиве нет программы"; rm -rf "$tmp_dir" "$tmp_archive"; return 1; }
[ -x "$TG_INIT_RS" ] && "$TG_INIT_RS" stop >/dev/null 2>&1 [ -x "$TG_INIT_RS" ] && "$TG_INIT_RS" stop >/dev/null 2>&1
rm -f "$TG_BIN_RS" _zm_bin_put "$1" "$TG_BIN_RS" || { rm -rf "$tmp_dir" "$tmp_archive"; [ -x "$TG_INIT_RS" ] && [ -x "$TG_BIN_RS" ] && "$TG_INIT_RS" start >/dev/null 2>&1; return 1; }
mv "$1" "$TG_BIN_RS" || { echo "ОШИБКА установки бинарника"; rm -rf "$tmp_dir" "$tmp_archive"; return 1; }
chmod +x "$TG_BIN_RS"
rm -rf "$tmp_dir" "$tmp_archive" rm -rf "$tmp_dir" "$tmp_archive"
if [ ! -f "$TG_SECRET_RS_FILE" ]; then if [ ! -f "$TG_SECRET_RS_FILE" ]; then
@@ -10854,8 +10928,20 @@ do_doh_install() {
echo "==> Устанавливаем https-dns-proxy и luci-app-https-dns-proxy" echo "==> Устанавливаем https-dns-proxy и luci-app-https-dns-proxy"
$INSTALL https-dns-proxy luci-app-https-dns-proxy >&2 || { echo "ОШИБКА установки"; return 1; } $INSTALL https-dns-proxy luci-app-https-dns-proxy >&2 || { echo "ОШИБКА установки"; return 1; }
_doh_force_apply _doh_force_apply
# Сервер по умолчанию — Quad9 (dns.quad9.net); пакет сам ставит Google и Cloudflare.
# Не заработал — пробуем Cloudflare и Google
local p r
for p in quad9 cloudflare google; do
echo "==> Включаем DNS через $(_doh_name "$p")"
if r="$(_doh_apply "$p")"; then
_doh_fo_put pref "$p"; _doh_fo_put active "$p"; rm -f "$DOH_FO_STATE"
echo " ✓ DNS роутера идёт через $(_doh_name "$p")"
break
fi
echo " ! $(printf '%s' "$r" | sed -n 's/.*"error":"\([^"]*\)".*/\1/p')"
done
_zo_doh_follow _zo_doh_follow
echo "==> Готово, DNS over HTTPS установлен — выберите сервер ниже" echo "==> Готово, DNS over HTTPS установлен — сервер можно сменить ниже"
} }
do_doh_remove() { do_doh_remove() {
@@ -12424,6 +12510,12 @@ stl_engine_install() {
mods="$(for m in $mods $(_stl_mods_present); do echo "$m"; done | awk '!s[$0]++' | tr '\n' ' ')" mods="$(for m in $mods $(_stl_mods_present); do echo "$m"; done | awk '!s[$0]++' | tr '\n' ' ')"
pkgs="steer-core$(for m in $mods; do printf ' steer-%s' "$m"; done)" pkgs="steer-core$(for m in $mods; do printf ' steer-%s' "$m"; done)"
stl_v2 && _pkg_is_installed steer-extended && pkgs="$pkgs steer-extended" stl_v2 && _pkg_is_installed steer-extended && pkgs="$pkgs steer-extended"
# только модули к стоящему ядру той же версии: качаем и ставим лишь недостающие пакеты
if [ -n "$STL_MODS_ONLY" ] && stl_v2 && [ "$(stl_version)" = "$ver" ]; then
pkgs="$(for p in $pkgs; do _pkg_is_installed "$p" || printf '%s ' "$p"; done)"
pkgs="$(echo $pkgs)"
[ -n "$pkgs" ] || { _stl_say "Нужные модули ядра steer уже стоят"; STL_NEW=""; STL_PKGS=""; return 0; }
fi
STL_NEW=""; STL_PKGS="$pkgs" STL_NEW=""; STL_PKGS="$pkgs"
for p in $pkgs; do _pkg_is_installed "$p" || STL_NEW="$STL_NEW $p"; done for p in $pkgs; do _pkg_is_installed "$p" || STL_NEW="$STL_NEW $p"; done
mkdir -p "$STL_TMP" mkdir -p "$STL_TMP"
@@ -12433,7 +12525,7 @@ stl_engine_install() {
files="$files $f" files="$files $f"
done done
stl_enabled && was_on=1 stl_enabled && was_on=1
_stl_say "Ставим ядро steer $ver: $(echo $pkgs | sed 's/ /, /g')" case " $pkgs " in *" steer-core "*) _stl_say "Ставим ядро steer $ver: $(echo $pkgs | sed 's/ /, /g')" ;; *) _stl_say "Ставим к ядру steer $ver: $(echo $pkgs | sed 's/ /, /g')" ;; esac
$UPDATE >&2 $UPDATE >&2
[ -s "$STL_SPEC" ] && cp -f "$STL_SPEC" "$keep" [ -s "$STL_SPEC" ] && cp -f "$STL_SPEC" "$keep"
if [ -n "$STL_REINSTALL" ] && [ -z "$legacy" ]; then if [ -n "$STL_REINSTALL" ] && [ -z "$legacy" ]; then
@@ -13390,9 +13482,15 @@ _st_ver_lt() {
_st_vpn_ok() { stl_has vpn || { stl_present && ! stl_v2; }; } _st_vpn_ok() { stl_has vpn || { stl_present && ! stl_v2; }; }
# STL_MODS_ONLY=1 — нужны только модули протоколов (новая VPN-секция, подписка с Hysteria2 и т. п.):
# ставятся модули ТОЙ ЖЕ версии, что и стоящее ядро, а само ядро не трогается. Раньше в этот момент ядро
# заодно обновлялось до последней версии — а после «Очистить» (она стирает запомненную последнюю версию)
# или выхода новой версии Steer при создании секции внезапно скачивал и ставил всё ядро заново, и на
# роутере с маленьким флешем это кончалось «недостаточно места». Обновить ядро — кнопка «Обновить»
_st_install_steer() { _st_install_steer() {
local want cur feat="$*" p local want cur feat="$*" p
want="$(stl_latest)"; cur="$(stl_version)" cur="$(stl_version)"
if [ -n "$STL_MODS_ONLY" ] && stl_v2; then want="$cur"; else want="$(stl_latest)"; fi
stl_v2 && _st_ver_lt "$want" "$cur" && want="$cur" stl_v2 && _st_ver_lt "$want" "$cur" && want="$cur"
[ -z "$feat" ] && [ -s "$ST_SUB" ] && feat="$(_st_sub_feats)" [ -z "$feat" ] && [ -s "$ST_SUB" ] && feat="$(_st_sub_feats)"
if stl_v2 && [ "$cur" = "$want" ] && _st_feats_ok $feat; then if stl_v2 && [ "$cur" = "$want" ] && _st_feats_ok $feat; then
@@ -13401,8 +13499,13 @@ _st_install_steer() {
fi fi
if stl_present && ! stl_v2; then _rb_say "Ядро steer $cur — переводим на $want" if stl_present && ! stl_v2; then _rb_say "Ядро steer $cur — переводим на $want"
elif stl_present && [ "$cur" != "$want" ]; then _rb_say "Ядро steer $cur — обновляем до $want" elif stl_present && [ "$cur" != "$want" ]; then _rb_say "Ядро steer $cur — обновляем до $want"
elif stl_v2; then _rb_say "Ставим модули ядра steer для протоколов подписки"; fi elif stl_v2; then _rb_say "Ставим модули ядра steer $cur для протоколов подписки — само ядро не трогаем"; fi
stl_engine_install "$want" $feat || return 1 if ! stl_engine_install "$want" $feat; then
# модулей для стоящей версии на сервере уже нет — тогда только обновлением ядра
[ -n "$STL_MODS_ONLY" ] && stl_v2 && p="$(stl_latest)" && [ "$p" != "$want" ] || return 1
_rb_say "Модулей для ядра steer $cur на сервере уже нет — обновляем ядро до $p вместе с модулями"
STL_MODS_ONLY="" stl_engine_install "$p" $feat || return 1
fi
if _st_owns "pkg steer" || _st_owns "pkg steer-extended"; then if _st_owns "pkg steer" || _st_owns "pkg steer-extended"; then
sed -i '/^pkg steer$/d; /^pkg steer-extended$/d' "$ST_OWNED" sed -i '/^pkg steer$/d; /^pkg steer-extended$/d' "$ST_OWNED"
STL_NEW="$STL_PKGS" STL_NEW="$STL_PKGS"
@@ -15283,7 +15386,7 @@ _st_engine_ready() {
local f local f
f="$(_st_feats_all)" f="$(_st_feats_all)"
stl_v2 && { [ -z "$(echo $f)" ] || _st_feats_ok $f; } && return 0 stl_v2 && { [ -z "$(echo $f)" ] || _st_feats_ok $f; } && return 0
_st_install_steer $f STL_MODS_ONLY=1 _st_install_steer $f
} }
_st_failopen() { stl_state "$(_st_exit)" && [ "$STL_FAILED" = true ]; } _st_failopen() { stl_state "$(_st_exit)" && [ "$STL_FAILED" = true ]; }
@@ -16957,7 +17060,7 @@ _st_need_ext() {
if ! stl_has vpn; then if ! stl_has vpn; then
_ensure_deps _ensure_deps
_st_phase pkgs _st_phase pkgs
_st_install_steer vpn || return 1 STL_MODS_ONLY=1 _st_install_steer vpn || return 1
stl_has vpn || { echo "ОШИБКА: модуль VLESS ядра steer не установился"; return 1; } stl_has vpn || { echo "ОШИБКА: модуль VLESS ядра steer не установился"; return 1; }
fi fi
_st_phase sub _st_phase sub
@@ -16970,7 +17073,7 @@ _st_need_mods() {
case " $f " in *" proxy "*) names="${names:+$names, }Trojan, Shadowsocks, SOCKS, HTTP или VMess" ;; esac case " $f " in *" proxy "*) names="${names:+$names, }Trojan, Shadowsocks, SOCKS, HTTP или VMess" ;; esac
_rb_say "В подписке есть серверы ${names:-VLESS} — ставим для них модули ядра steer" _rb_say "В подписке есть серверы ${names:-VLESS} — ставим для них модули ядра steer"
_st_phase pkgs _st_phase pkgs
_st_install_steer $f || return 1 STL_MODS_ONLY=1 _st_install_steer $f || return 1
_st_feats_ok $f || { echo "ОШИБКА: модули ядра steer для протоколов подписки не установились"; return 1; } _st_feats_ok $f || { echo "ОШИБКА: модули ядра steer для протоколов подписки не установились"; return 1; }
_st_phase sub _st_phase sub
} }
@@ -21489,7 +21592,7 @@ _fk_zash_ensure() {
} }
FK_REPO="yandexru45/netshift" FK_REPO="yandexru45/netshift"
FK_PIN="0.9.11" FK_PIN="0.9.12"
FK_MARK="/usr/lib/netshift/zm-managed" FK_MARK="/usr/lib/netshift/zm-managed"
FK_UC="/opt/zapret-manager-luci/netshift.uc" FK_UC="/opt/zapret-manager-luci/netshift.uc"
FK_SB_OWN="$ZM_STATE_DIR/netshift.singbox" FK_SB_OWN="$ZM_STATE_DIR/netshift.singbox"
@@ -22177,30 +22280,6 @@ _fk_b4_watch() {
return 0 return 0
} }
# Подписки, которые панель до 4.44 переключила на формат Happ (из-за этого могли пропасть серверы
# Hysteria2 и VMess): один раз проверяем их заново — Forkozz применяет настройки сам, не чаще раза в 30 минут
_fk_subfmt_watch() {
local f="$JOBS_DIR/fk-subfmt.last" d="$ZM_STATE_DIR/fk.subfmt.day" s
# раз в сутки: сервис мог начать отдавать серверы (например, Hysteria2) только в одном формате —
# сверяем форматы подписок с «Авто» и, если выбор поменялся, сразу перекачиваем подписку
if grep -q "proxy_config_type 'subscription'" /etc/config/netshift 2>/dev/null && [ -z "$(find "$d" -mmin -1440 2>/dev/null)" ] &&
_fk_up && ! _job_running forkop; then
mkdir -p "$ZM_STATE_DIR"; : > "$d"
( _fk_sub_fmt_auto >/dev/null 2>&1 && _zm_run 300 /usr/bin/netshift subscription_update all >/dev/null 2>&1 ) </dev/null >/dev/null 2>&1 &
return 0
fi
grep -q "subscription_format_preference 'xray'" /etc/config/netshift 2>/dev/null || return 0
[ -n "$(find "$f" -mmin -30 2>/dev/null)" ] && return 0
for s in $(uci -q show netshift | sed -n "s/^netshift\.\([A-Za-z0-9_]*\)\.subscription_format_preference='xray'$/\1/p"); do
[ -z "$(uci -q get "netshift.$s.zm_subfmt_auto")" ] && [ -f "$ZM_STATE_DIR/fk.subfmt.$s" ] || continue
_fk_up && ! _job_running forkop || return 0
mkdir -p "$JOBS_DIR"; : > "$f"
job_start forkop do_fk_service apply >/dev/null 2>&1
return 0
done
return 0
}
_fk_ghdl_patch() { _fk_ghdl_patch() {
local f=/usr/lib/netshift/helpers.sh local f=/usr/lib/netshift/helpers.sh
[ -f "$f" ] || return 0 [ -f "$f" ] || return 0
@@ -23270,12 +23349,13 @@ _fk_why() {
# только записи этого запуска: старая ошибка из журнала (например, «нет настроенного подключения» до # только записи этого запуска: старая ошибка из журнала (например, «нет настроенного подключения» до
# того, как добавили сервер) больше не выдаётся за причину сейчас # того, как добавили сервер) больше не выдаётся за причину сейчас
if [ -n "$FK_LOGMARK" ]; then if [ -n "$FK_LOGMARK" ]; then
l="$(logread 2>/dev/null | awk -v m="$FK_LOGMARK" 'f { print } index($0, m) { f = 1 }' | grep -E 'netshift|sing-box' | grep -E 'fatal|FATAL|Aborted|error' | tail -n 1)" l="$(logread 2>/dev/null | awk -v m="$FK_LOGMARK" 'f { print } index($0, m) { f = 1 }' | grep -E 'netshift|sing-box' | grep -E 'fatal|FATAL|Aborted|error|is not started: no section' | tail -n 1)"
else else
l="$(logread 2>/dev/null | tail -n 200 | grep -E 'netshift|sing-box' | grep -E 'fatal|FATAL|Aborted|error' | tail -n 1)" l="$(logread 2>/dev/null | tail -n 200 | grep -E 'netshift|sing-box' | grep -E 'fatal|FATAL|Aborted|error|is not started: no section' | tail -n 1)"
fi fi
case "$l" in case "$l" in
*"initial rule-set"*) echo "sing-box не смог скачать набор правил при запуске (подробности выше)" ;; *"initial rule-set"*) echo "sing-box не смог скачать набор правил при запуске (подробности выше)" ;;
*"no section has a connection yet"*) echo "ни в одной секции нет подключения — добавьте ссылку на сервер или подписку" ;;
*"Outbound section not found"*) echo "ни в одной секции нет настроенного подключения" ;; *"Outbound section not found"*) echo "ни в одной секции нет настроенного подключения" ;;
*"Invalid service in community lists"*) echo "в секции выбран список, которого Forkozz не знает — сохраните сервисы заново" ;; *"Invalid service in community lists"*) echo "в секции выбран список, которого Forkozz не знает — сохраните сервисы заново" ;;
*"configuration"*"is invalid"*) echo "sing-box не принял собранный конфиг — посмотрите журнал" ;; *"configuration"*"is invalid"*) echo "sing-box не принял собранный конфиг — посмотрите журнал" ;;
@@ -23491,88 +23571,69 @@ _fk_sub_keys() {
end' 2>/dev/null end' 2>/dev/null
} }
# Подписка: какой формат у сервиса полнее — sing-box или Xray JSON (Happ). Сервер сравнивается целиком: # Формат подписки (sing-box или Xray JSON / Happ) панель определяет ОДИН раз — когда подписка добавлена
# протокол + адрес + порт. Раньше сравнивались только адрес и порт, а у многих сервисов Hysteria2 (UDP) и # (её ссылки ещё не было) и формат стоит «Авто». Скачивает подписку в обоих форматах и берёт тот, где
# VLESS (TCP) живут на одном адресе и порту 443 — панель считала, что в Happ «есть всё», переключала # серверов видно больше (сервер сравнивается целиком: протокол + адрес + порт). Решение запоминается
# подписку на Happ, и серверы Hysteria2 (а заодно VMess, которых в Happ нет) пропадали из списка. # (zm_subfmt_url — отпечаток ссылки) и больше не пересматривается: ни при применении настроек, ни раз в
# Переключённые так подписки (отметка zm_subfmt_auto) проверяются снова и возвращаются на «Авто», # сутки, ни при обновлении подписки. Формат, выбранный вручную, панель не трогает никогда.
# если в Happ чего-то не хватает. Формат, выбранный вручную, панель не трогает # Раньше панель перепроверяла формат и могла вернуть подписку с Happ на «Авто» — у части сервисов из-за
# этого пропадали серверы Hysteria2, даже если Happ выбрали вручную
_fk_sub_fmt_auto() { _fk_sub_fmt_auto() {
local sec url ver sb xr ch="" d="$JOBS_DIR/fk-subfmt" mark n_sb n_xr miss xex lost extra pref auto legacy local sec url key ver ch="" sw="" d="$JOBS_DIR/fk-subfmt" n_sb n_xr miss xex lost extra pref
command -v jq >/dev/null 2>&1 || return 1 command -v jq >/dev/null 2>&1 || return 1
ver="$(sing-box version 2>/dev/null | head -n1 | awk '{print $3}')" ver="$(sing-box version 2>/dev/null | head -n1 | awk '{print $3}')"
for sec in $(uci -q show netshift | sed -n "s/^netshift\.\([A-Za-z0-9_]*\)\.subscription_url=.*/\1/p" | sort -u); do for sec in $(uci -q show netshift | sed -n "s/^netshift\.\([A-Za-z0-9_]*\)\.subscription_url=.*/\1/p" | sort -u); do
pref="$(uci -q get "netshift.$sec.subscription_format_preference")"
mark="$ZM_STATE_DIR/fk.subfmt.$sec"
auto=""
case "$pref" in
''|auto) ;;
xray)
# переключила панель: есть её отметка, или (до 4.44) она проверяла эту подписку
if [ "$(uci -q get "netshift.$sec.zm_subfmt_auto")" = 1 ] || { [ -f "$mark" ] && [ -z "$(uci -q get "netshift.$sec.zm_subfmt_auto")" ]; }; then auto=1
else continue; fi ;;
*) continue ;;
esac
[ -z "$(uci -q get "netshift.$sec.subscription_user_agent")" ] || continue
[ "$(uci -q get "netshift.$sec.proxy_config_type")" = subscription ] || continue [ "$(uci -q get "netshift.$sec.proxy_config_type")" = subscription ] || continue
url="$(uci -q get "netshift.$sec.subscription_url")" url="$(uci -q get "netshift.$sec.subscription_url")"
case "$url" in *' '*) continue ;; esac key="$(printf '%s' "$url" | md5sum | cut -c1-16)"
case "$url" in http://*|https://*) ;; *) continue ;; esac [ "$(uci -q get "netshift.$sec.zm_subfmt_url")" = "$key" ] && continue
# подписку, переключённую на Happ до 4.44, проверяем сразу; остальные — не чаще раза в сутки pref="$(uci -q get "netshift.$sec.subscription_format_preference")"
legacy=""; [ -n "$auto" ] && [ -z "$(uci -q get "netshift.$sec.zm_subfmt_auto")" ] && legacy=1 # старые подписки (панель их уже проверяла до 4.54) и подписки с форматом/User-Agent, выбранными
if [ "$1" != force ] && [ -z "$legacy" ]; then # вручную, — запоминаем как есть, ничего не меняя
[ -f "$mark" ] && [ "$(cat "$mark" 2>/dev/null)" = "$(printf '%s' "$url" | md5sum | cut -c1-16)" ] && [ -z "$(find "$mark" -mmin +1440 2>/dev/null)" ] && continue if [ "$(cat "$ZM_STATE_DIR/fk.subfmt.$sec" 2>/dev/null)" = "$key" ] || { [ -n "$pref" ] && [ "$pref" != auto ]; } ||
[ -n "$(uci -q get "netshift.$sec.subscription_user_agent")$(uci -q get "netshift.$sec.subscription_hwid")$(uci -q get "netshift.$sec.subscription_device_os")" ]; then
uci set "netshift.$sec.zm_subfmt_url=$key"; uci -q delete "netshift.$sec.zm_subfmt_auto"
rm -f "$ZM_STATE_DIR/fk.subfmt.$sec"; ch=1
continue
fi fi
_fk_say "Подписка «$(uci -q get "netshift.$sec.zm_label" || echo "$sec")»: сравниваем, что сервер отдаёт в форматах sing-box и Happ" case "$url" in *' '*|'') continue ;; esac
case "$url" in http://*|https://*) ;; *) continue ;; esac
_fk_say "Новая подписка «$(uci -q get "netshift.$sec.zm_label" || echo "$sec")»: определяем формат — сравниваем, что сервер отдаёт в форматах sing-box и Happ"
rm -rf "$d"; mkdir -p "$d"; : > "$d/sb"; : > "$d/xr" rm -rf "$d"; mkdir -p "$d"; : > "$d/sb"; : > "$d/xr"
curl -fsSL --compressed -m 15 -A "singbox/${ver:-1.12.0}" -o "$d/sb" "$url" 2>/dev/null curl -fsSL --compressed -m 15 -A "singbox/${ver:-1.12.0}" -o "$d/sb" "$url" 2>/dev/null
curl -fsSL --compressed -m 15 -A "Happ/1.0.0" -o "$d/xr" "$url" 2>/dev/null curl -fsSL --compressed -m 15 -A "Happ/1.0.0" -o "$d/xr" "$url" 2>/dev/null
_fk_sub_keys < "$d/sb" | sort -u > "$d/sb.k"; _fk_sub_keys < "$d/xr" | sort -u > "$d/xr.k" _fk_sub_keys < "$d/sb" | sort -u > "$d/sb.k"; _fk_sub_keys < "$d/xr" | sort -u > "$d/xr.k"
mkdir -p "$ZM_STATE_DIR"; printf '%s' "$url" | md5sum | cut -c1-16 > "$mark"
n_sb="$(grep -c . "$d/sb.k")"; n_xr="$(grep -c . "$d/xr.k")" n_sb="$(grep -c . "$d/sb.k")"; n_xr="$(grep -c . "$d/xr.k")"
if [ "$n_sb" = 0 ] && [ "$n_xr" = 0 ]; then echo " · подписка не скачалась напрямую — формат оставляем как есть"; rm -rf "$d"; continue; fi # не скачалась — формат определим при следующем применении настроек
# сравниваем серверы целиком (протокол + адрес + порт): каких нет в Happ (miss) и каких нет в sing-box (xex) if [ "$n_sb" = 0 ] && [ "$n_xr" = 0 ]; then echo " · подписка не скачалась напрямую — формат определим в следующий раз, пока «Авто»"; rm -rf "$d"; continue; fi
uci set "netshift.$sec.zm_subfmt_url=$key"; ch=1
miss="$(grep -vxF -f "$d/xr.k" "$d/sb.k" | grep -c .)"; [ -s "$d/xr.k" ] || miss="$n_sb" miss="$(grep -vxF -f "$d/xr.k" "$d/sb.k" | grep -c .)"; [ -s "$d/xr.k" ] || miss="$n_sb"
xex="$(grep -vxF -f "$d/sb.k" "$d/xr.k" | grep -c .)"; [ -s "$d/sb.k" ] || xex="$n_xr" xex="$(grep -vxF -f "$d/sb.k" "$d/xr.k" | grep -c .)"; [ -s "$d/sb.k" ] || xex="$n_xr"
# Берём формат, в котором серверов видно больше всего: Happ — если в нём есть серверы, которых нет в # Happ — если в нём есть серверы, которых нет в sing-box, и он не теряет серверов sing-box,
# sing-box, и он не теряет серверов sing-box, а если у каждого формата есть свои — тот, где их больше # а если у каждого формата есть свои — тот, где серверов больше
if [ "$n_xr" -gt 0 ] && [ "$xex" -gt 0 ] && { [ "$miss" = 0 ] || [ "$n_xr" -gt "$n_sb" ]; }; then if [ "$n_xr" -gt 0 ] && [ "$xex" -gt 0 ] && { [ "$miss" = 0 ] || [ "$n_xr" -gt "$n_sb" ]; }; then
extra="$(grep -vxF -f "$d/sb.k" "$d/xr.k" | cut -d'|' -f1 | sort -u | tr '\n' ' ' | sed 's/ $//')" extra="$(grep -vxF -f "$d/sb.k" "$d/xr.k" | cut -d'|' -f1 | sort -u | tr '\n' ' ' | sed 's/ $//')"
lost=""; [ "$miss" = 0 ] || lost="$(grep -vxF -f "$d/xr.k" "$d/sb.k" | cut -d'|' -f1 | sort -u | tr '\n' ' ' | sed 's/ $//')" lost=""; [ "$miss" = 0 ] || lost="$(grep -vxF -f "$d/xr.k" "$d/sb.k" | cut -d'|' -f1 | sort -u | tr '\n' ' ' | sed 's/ $//')"
if [ -n "$auto" ] && [ "$pref" = xray ]; then uci set "netshift.$sec.subscription_format_preference=xray"; sw=1
uci set "netshift.$sec.zm_subfmt_auto=1"; ch=1 echo " ✓ в формате Happ серверов больше ($n_xr против $n_sb${extra:+, в том числе $extra}) — подписке поставлен формат Happ"
echo " ✓ формат Happ по-прежнему полнее ($n_xr серверов против $n_sb) — оставляем" [ -n "$lost" ] && echo " ! в формате Happ нет $miss серверов из формата sing-box ($lost)"
else logger -t zapret-manager "Forkozz: новой подписке секции $sec поставлен формат Happ ($n_xr серверов против $n_sb)"
uci set "netshift.$sec.subscription_format_preference=xray" elif [ "$n_xr" = 0 ]; then echo " ✓ формат «Авто» (sing-box): в формате Happ сервер подписку не отдал"
uci set "netshift.$sec.zm_subfmt_auto=1" elif [ "$xex" = 0 ]; then echo " ✓ формат «Авто» (sing-box): в Happ нет серверов, которых нет в нём ($n_sb)"
ch=1 elif [ "$n_sb" = "$n_xr" ]; then echo " ✓ формат «Авто» (sing-box): серверов столько же ($n_sb), но $xex есть только в Happ, а $miss — только в sing-box. Нужны именно они — выберите Happ вручную"
echo " ✓ в формате Happ серверов больше ($n_xr против $n_sb${extra:+, в том числе $extra}) — подписка переключена на формат Happ" else echo " ✓ формат «Авто» (sing-box): в нём серверов больше ($n_sb против $n_xr), хотя $xex есть только в Happ. Нужны именно они — выберите Happ вручную"; fi
logger -t zapret-manager "Forkozz: подписка секции $sec переключена на формат Happ ($n_xr серверов против $n_sb)" echo " · формат запомнен и больше сам не меняется — поменять можно вручную в настройках подписки"
fi
[ -n "$lost" ] && echo " ! в формате Happ нет $miss серверов из формата sing-box ($lost) — сервис отдаёт их только для sing-box, а их меньше"
rm -rf "$d"; continue
fi
if [ -n "$auto" ]; then
uci -q delete "netshift.$sec.subscription_format_preference"
uci -q delete "netshift.$sec.zm_subfmt_auto"
ch=1
extra="$(grep -vxF -f "$d/xr.k" "$d/sb.k" | cut -d'|' -f1 | sort -u | tr '\n' ' ' | sed 's/ $//')"
echo " ✓ подписка возвращена на «Авто»: в формате sing-box серверов не меньше ($n_sb против $n_xr${extra:+; только в sing-box — $extra})"
logger -t zapret-manager "Forkozz: подписка секции $sec возвращена с формата Happ на «Авто» ($n_sb серверов в sing-box против $n_xr в Happ)"
elif [ "$n_xr" = 0 ]; then echo " ✓ формат sing-box подходит: в формате Happ сервер подписку не отдал"
elif [ "$xex" = 0 ]; then echo " ✓ формат sing-box подходит: в Happ нет серверов, которых нет в нём ($n_sb)"
else echo " ✓ формат sing-box оставляем: в нём серверов больше ($n_sb против $n_xr), хотя $xex есть только в Happ"; fi
rm -rf "$d" rm -rf "$d"
done done
[ -n "$ch" ] || return 1 [ -n "$ch" ] || return 1
uci commit netshift uci commit netshift
return 0 # 0 — у новой подписки поставлен формат Happ: её надо перекачать
[ -n "$sw" ]
} }
# Обновление подписок с понятным ходом работы. Сам движок Forkozz 0.9.11 подписку в формате Happ (Xray # Обновление подписок с понятным ходом работы. Движок Forkozz 0.9.11 подписку в формате Happ (Xray
# JSON) разбирает медленно: каждый сервер — отдельной ссылкой, примерно десяток процессов на сервер, — # JSON) разбирает медленно: каждый сервер — отдельной ссылкой, — на роутере это минута-две на полсотни
# на роутере это минута-две на полсотни серверов. Раньше всё это время шли одни точки «Подождите…»; # серверов. С 0.9.12 (linkfast.jq) такие подписки разбираются одним проходом jq — за секунды
# теперь видно, что происходит. В следующей версии движка такие подписки разбираются одним проходом
_fk_sub_update() { _fk_sub_update() {
local p m="zm-sub-$$-$(date +%s)" seen=0 tot all i=0 rc=0 out="$JOBS_DIR/fk-sub.out" local p m="zm-sub-$$-$(date +%s)" seen=0 tot all i=0 rc=0 out="$JOBS_DIR/fk-sub.out"
mkdir -p "$JOBS_DIR" mkdir -p "$JOBS_DIR"
@@ -23585,14 +23646,14 @@ _fk_sub_update() {
all="$(logread 2>/dev/null | awk -v m="$m" 'f && /netshift/ { print } index($0, m) { f = 1 }')" all="$(logread 2>/dev/null | awk -v m="$m" 'f && /netshift/ { print } index($0, m) { f = 1 }')"
tot="$(printf '%s\n' "$all" | grep -c .)" tot="$(printf '%s\n' "$all" | grep -c .)"
[ "$tot" -gt "$seen" ] || continue [ "$tot" -gt "$seen" ] || continue
printf '%s\n' "$all" | awk -v skip="$seen" ' printf '%s\n' "$all" | awk -v skip="$seen" -v fast="$([ -f /usr/lib/netshift/linkfast.jq ] && echo 1)" '
NR <= skip { next } NR <= skip { next }
{ sub(/^.*netshift[^:]*: /, ""); sub(/^\[[a-z]+\] /, "") } { sub(/^.*netshift[^:]*: /, ""); sub(/^\[[a-z]+\] /, "") }
/Subscription connectivity check passed/ { print " ✓ Сервер подписки отвечает"; next } /Subscription connectivity check passed/ { print " ✓ Сервер подписки отвечает"; next }
/Subscription connectivity check failed/ { print " ✗ Сервер подписки не отвечает"; next } /Subscription connectivity check failed/ { print " ✗ Сервер подписки не отвечает"; next }
/Trying subscription User-Agent/ { u = $0; sub(/^.*: /, "", u); f = (u ~ /^[Hh]app|^v2ray/) ? "Happ" : (u ~ /^sing-?box/) ? "sing-box" : u; print " → Просим подписку в формате " f; next } /Trying subscription User-Agent/ { u = $0; sub(/^.*: /, "", u); f = (u ~ /^[Hh]app|^v2ray/) ? "Happ" : (u ~ /^sing-?box/) ? "sing-box" : u; print " → Просим подписку в формате " f; next }
/Downloaded subscription body/ { b = $0; sub(/^.*bytes=/, "", b); sub(/[^0-9].*$/, "", b); if (b + 0 > 0) printf " ✓ Скачано %d КБ\n", (b + 1023) / 1024; next } /Downloaded subscription body/ { b = $0; sub(/^.*bytes=/, "", b); sub(/[^0-9].*$/, "", b); if (b + 0 > 0) printf " ✓ Скачано %d КБ\n", (b + 1023) / 1024; next }
/Detected Xray JSON subscription/ { print " → Формат Happ: движок Forkozz разбирает серверы по одному — на роутере это минута-две, дождитесь"; next } /Detected Xray JSON subscription/ { print (fast ? " → Формат Happ: разбираем серверы" : " → Формат Happ: движок Forkozz разбирает серверы по одному — на роутере это минута-две, дождитесь"); next }
/produced [0-9]+ outbound/ { n = $0; sub(/^.*produced /, "", n); sub(/ .*$/, "", n); print " ✓ Разобрано серверов: " n; next } /produced [0-9]+ outbound/ { n = $0; sub(/^.*produced /, "", n); sub(/ .*$/, "", n); print " ✓ Разобрано серверов: " n; next }
/Subscription download failed/ { print " ! Подписка не скачалась — пробуем другой формат"; next } /Subscription download failed/ { print " ! Подписка не скачалась — пробуем другой формат"; next }
/Applying updated subscriptions/ { print " → Применяем новые серверы в sing-box"; next } /Applying updated subscriptions/ { print " → Применяем новые серверы в sing-box"; next }
@@ -23610,7 +23671,7 @@ _fk_sub_update() {
do_fk_subs() { do_fk_subs() {
_fk_installed || { echo "ОШИБКА: Forkozz не установлен"; return 1; } _fk_installed || { echo "ОШИБКА: Forkozz не установлен"; return 1; }
_fk_say "Обновляем подписки" _fk_say "Обновляем подписки"
_fk_sub_fmt_auto force _fk_sub_fmt_auto
local lx; lx="$(_fk_log_ext)" local lx; lx="$(_fk_log_ext)"
_fk_mlkem_patch _fk_mlkem_patch
_fk_serial_patch _fk_serial_patch
@@ -24872,6 +24933,7 @@ do_sysinfo() {
elif [ "$sw" = 1 ]; then _si_row "Flow Offloading" "программный" warn elif [ "$sw" = 1 ]; then _si_row "Flow Offloading" "программный" warn
else _si_row "Flow Offloading" "выключен" ok; fi else _si_row "Flow Offloading" "выключен" ok; fi
[ "$fix" = true ] && _si_row "FIX для Flow Offloading" "применён" ok || _si_row "FIX для Flow Offloading" "не применён" "$([ "$sw" = 1 ] || [ "$hw" = 1 ] && echo bad)" [ "$fix" = true ] && _si_row "FIX для Flow Offloading" "применён" ok || _si_row "FIX для Flow Offloading" "не применён" "$([ "$sw" = 1 ] || [ "$hw" = 1 ] && echo bad)"
stl_present && { _ft_steer_applied && _si_row "Steer и Flow Offloading" "соединения Steer не выгружаются" ok || _si_row "Steer и Flow Offloading" "соединения Steer выгружаются" "$([ "$sw" = 1 ] || [ "$hw" = 1 ] && echo bad)"; }
if /etc/init.d/https-dns-proxy running >/dev/null 2>&1 && [ -f /etc/config/https-dns-proxy ]; then if /etc/init.d/https-dns-proxy running >/dev/null 2>&1 && [ -f /etc/config/https-dns-proxy ]; then
sed -n "s/^[[:space:]]*option resolver_url '\([^']*\)'.*/\1/p" /etc/config/https-dns-proxy | while IFS= read -r v; do _si_row "DNS over HTTPS" "$v" ""; SI_SEP=","; done sed -n "s/^[[:space:]]*option resolver_url '\([^']*\)'.*/\1/p" /etc/config/https-dns-proxy | while IFS= read -r v; do _si_row "DNS over HTTPS" "$v" ""; SI_SEP=","; done
fi fi
@@ -27031,6 +27093,10 @@ function sec_ext(m) {
sub_incl: join(" | ", kw_list(m.subscription_filter_include_keywords)), sub_incl: join(" | ", kw_list(m.subscription_filter_include_keywords)),
sub_insecure: flag(m.subscription_insecure), sub_insecure: flag(m.subscription_insecure),
sub_ua: s(m.subscription_user_agent), sub_ua: s(m.subscription_user_agent),
sub_hwid: s(m.subscription_hwid),
sub_os: s(m.subscription_device_os),
sub_osv: s(m.subscription_os_version),
sub_model: s(m.subscription_device_model),
ut_iv: index(UT_IVS, s(m.urltest_check_interval)) >= 0 ? s(m.urltest_check_interval) : "3m", ut_iv: index(UT_IVS, s(m.urltest_check_interval)) >= 0 ? s(m.urltest_check_interval) : "3m",
ut_tol: s(num_in(m.urltest_tolerance, 0, 10000) ?? 50), ut_tol: s(num_in(m.urltest_tolerance, 0, 10000) ?? 50),
ut_url: s(m.urltest_testing_url) || UT_URL_DEF, ut_url: s(m.urltest_testing_url) || UT_URL_DEF,
@@ -27064,6 +27130,7 @@ function set_ext(st) {
ipv6: flag(st.enable_ipv6), ipv6: flag(st.enable_ipv6),
no_dhcp: flag(st.dont_touch_dhcp), no_dhcp: flag(st.dont_touch_dhcp),
excl_ntp: flag(st.exclude_ntp), excl_ntp: flag(st.exclude_ntp),
wifi_call: flag(st.wifi_calling_direct),
excl_bt: flag(st.exclude_bittorrent), excl_bt: flag(st.exclude_bittorrent),
bt_ports: s(st.zm_bt_ports), bt_ports: s(st.zm_bt_ports),
block_doh: flag(st.block_doh), block_doh: flag(st.block_doh),
@@ -27143,7 +27210,8 @@ function set_flag(c, sec, key, on) {
} }
function write_sec_ext(c, sec, mode, x) { function write_sec_ext(c, sec, mode, x) {
const SUB_KEYS = [ "subscription_format_preference", "zm_subfmt_auto", "subscription_insecure", "subscription_user_agent", "subscription_group_mode", "subscription_group_prefix_len", "group_by_countries", "subscription_filter_include_keywords", "zm_sub_priority" ]; const SUB_KEYS = [ "subscription_format_preference", "zm_subfmt_auto", "zm_subfmt_url", "subscription_insecure", "subscription_user_agent", "subscription_group_mode", "subscription_group_prefix_len", "group_by_countries", "subscription_filter_include_keywords", "zm_sub_priority",
"subscription_hwid", "subscription_device_os", "subscription_os_version", "subscription_device_model" ];
const UT_KEYS = [ "urltest_check_interval", "urltest_tolerance", "urltest_testing_url" ]; const UT_KEYS = [ "urltest_check_interval", "urltest_tolerance", "urltest_testing_url" ];
const DRES_KEYS = [ "domain_resolver_dns_type", "domain_resolver_dns_server" ]; const DRES_KEYS = [ "domain_resolver_dns_type", "domain_resolver_dns_server" ];
let cur = sec_ext(c.get_all(CFG, sec) || {}); let cur = sec_ext(c.get_all(CFG, sec) || {});
@@ -27163,6 +27231,13 @@ function write_sec_ext(c, sec, mode, x) {
set_flag(c, sec, "zm_sub_priority", !!x.sub_priority); set_flag(c, sec, "zm_sub_priority", !!x.sub_priority);
set_flag(c, sec, "subscription_insecure", !!x.sub_insecure); set_flag(c, sec, "subscription_insecure", !!x.sub_insecure);
set_opt(c, sec, "subscription_user_agent", ua, ""); set_opt(c, sec, "subscription_user_agent", ua, "");
// «устройство» для подписки (движок 0.9.12+): уходит в заголовки X-HWID, X-Device-OS, X-Ver-OS, X-Device-Model
for (let kv in [ [ "sub_hwid", "subscription_hwid", "HWID", 128 ], [ "sub_os", "subscription_device_os", "ОС устройства", 40 ],
[ "sub_osv", "subscription_os_version", "версия ОС", 40 ], [ "sub_model", "subscription_device_model", "модель устройства", 80 ] ]) {
let v = trim(s(x[kv[0]]));
if (length(v) > kv[3] || match(v, /[\r\n\t'"\\`$]/) || non_ascii(v)) fail(kv[2] + " для подписки — одна строка латиницей до " + kv[3] + " символов, без кавычек");
set_opt(c, sec, kv[1], v, "");
}
c.delete(CFG, sec, "group_by_countries"); c.delete(CFG, sec, "group_by_countries");
set_opt(c, sec, "subscription_group_mode", grp, "off"); set_opt(c, sec, "subscription_group_mode", grp, "off");
set_opt(c, sec, "subscription_group_prefix_len", grp == "prefix" ? s(plen) : null, "2"); set_opt(c, sec, "subscription_group_prefix_len", grp == "prefix" ? s(plen) : null, "2");
@@ -27269,7 +27344,10 @@ function engine_caps() {
prefixes: prefix_ok(), prefixes: prefix_ok(),
// ссылки tuic:// и anytls:// движок понимает с версий новее 0.9.11 — разрешаем их, как только он их знает // ссылки tuic:// и anytls:// движок понимает с версий новее 0.9.11 — разрешаем их, как только он их знает
tuic: index(fc, "\n tuic)") >= 0, tuic: index(fc, "\n tuic)") >= 0,
anytls: index(fc, "\n anytls)") >= 0 anytls: index(fc, "\n anytls)") >= 0,
// с движка 0.9.12: звонки по Wi‑Fi напрямую и «устройство» для подписки (HWID, ОС, модель)
wifi_call: index(b, "wifi_calling_direct") >= 0,
sub_id: index(s(fs.readfile(NS_LIB + "/helpers.sh")), "load_subscription_client_identity()") >= 0
}; };
} }
@@ -27316,6 +27394,7 @@ function write_set_ext(c, g) {
for (let kv in [ [ "ipv6", "enable_ipv6" ], [ "no_dhcp", "dont_touch_dhcp" ], [ "excl_ntp", "exclude_ntp" ], [ "excl_bt", "exclude_bittorrent" ], [ "block_doh", "block_doh" ] ]) for (let kv in [ [ "ipv6", "enable_ipv6" ], [ "no_dhcp", "dont_touch_dhcp" ], [ "excl_ntp", "exclude_ntp" ], [ "excl_bt", "exclude_bittorrent" ], [ "block_doh", "block_doh" ] ])
c.set(CFG, S, kv[1], g[kv[0]] ? "1" : "0"); c.set(CFG, S, kv[1], g[kv[0]] ? "1" : "0");
set_flag(c, S, "wifi_calling_direct", !!g.wifi_call);
let btl = []; let btl = [];
for (let p in split(replace(trim(s(g.bt_ports)), /[ \t;]+/g, ","), ",")) { for (let p in split(replace(trim(s(g.bt_ports)), /[ \t;]+/g, ","), ",")) {
if (p == "") continue; if (p == "") continue;
@@ -45613,10 +45692,24 @@ var UT_URL_DEF = 'https://www.gstatic.com/generate_204';
var UT_URLS = [ { id: UT_URL_DEF, label: 'Google' }, { id: 'https://cp.cloudflare.com/generate_204', label: 'Cloudflare' }, { id: 'https://captive.apple.com', label: 'Apple' } ]; var UT_URLS = [ { id: UT_URL_DEF, label: 'Google' }, { id: 'https://cp.cloudflare.com/generate_204', label: 'Cloudflare' }, { id: 'https://captive.apple.com', label: 'Apple' } ];
var LOG_LV = [ { id: 'panic', label: 'Выключен' }, { id: 'error', label: 'Только ошибки' }, { id: 'warn', label: 'Обычный' }, { id: 'info', label: 'Подробный' }, { id: 'debug', label: 'Отладка' } ]; var LOG_LV = [ { id: 'panic', label: 'Выключен' }, { id: 'error', label: 'Только ошибки' }, { id: 'warn', label: 'Обычный' }, { id: 'info', label: 'Подробный' }, { id: 'debug', label: 'Отладка' } ];
var DRES_DEF = { udp: '1.1.1.1', dot: 'one.one.one.one', doh: 'https://cloudflare-dns.com/dns-query' }; var DRES_DEF = { udp: '1.1.1.1', dot: 'one.one.one.one', doh: 'https://cloudflare-dns.com/dns-query' };
var SEC_EXT = { sub_fmt: 'auto', sub_priority: false, ping_auto: false, sub_group: 'off', sub_plen: '2', sub_incl: '', sub_insecure: false, sub_ua: '', ut_iv: '3m', ut_tol: '50', ut_url: UT_URL_DEF, ut_guard: false, direct_fallback: false, // «Устройство» для подписки — как в генераторе клиента движка NetShift 0.9.12: User-Agent, HWID, ОС,
// версия ОС и модель так, как их шлёт приложение на этой платформе. HWID каждый раз новый
var SUB_CLIENT_IPHONES = [ 'iPhone 15 Pro Max', 'iPhone 16 Pro Max', 'iPhone 17 Pro', 'iPhone 13', 'iPhone 16', 'iPhone 15 Pro' ];
function subRnd(abc, n) { var o = ''; for (var i = 0; i < n; i++) o += abc.charAt(Math.floor(Math.random() * abc.length)); return o; }
function subPick(a) { return a[Math.floor(Math.random() * a.length)]; }
function subUuid() { var h = subRnd('0123456789abcdef', 32); return [ h.slice(0, 8), h.slice(8, 12), '4' + h.slice(13, 16), subPick('89ab'.split('')) + h.slice(17, 20), h.slice(20) ].join('-'); }
function subIncyId() { return subRnd('0123456789ABCDEF', 32).replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, '$1-$2-$3-$4-$5'); }
var SUB_CLIENTS = [
{ id: 'happ_android', label: 'Happ · Android', make: function() { return { ua: 'Happ/4.7.1/Android/' + subRnd('123456789', 1) + subRnd('0123456789', 19), hwid: subRnd('0123456789abcdef', 16), os: 'Android', osv: subPick([ '16', '15', '14', '13' ]), model: subPick([ 'CPH2747', 'SM-A556E', 'SM-S721B', 'SM-S938B', 'SM-S918N', '2112123AG' ]) }; } },
{ id: 'happ_ios', label: 'Happ · iOS', make: function() { return { ua: 'Happ/6.1.0/ios/2610091516618', hwid: subRnd('0123456789abcdefghijklmnopqrstuvwxyz', 16), os: 'iOS', osv: subPick([ '26.6.2', '26.6.1', '27.0.1' ]), model: subPick(SUB_CLIENT_IPHONES) }; } },
{ id: 'happ_win', label: 'Happ · Windows', make: function() { return { ua: 'Happ/4.5.4/Windows/2610141139602', hwid: subUuid(), os: 'Windows', osv: subPick([ '11_10.0.26200', '10_10.0.19045', '11_10.0.26100' ]), model: 'DESKTOP-' + subRnd('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', 7) + '_x86_64' }; } },
{ id: 'incy_android', label: 'INCY · Android', make: function() { return { ua: 'INCY/3.7.9/android Dalvik/2.1.0', hwid: subIncyId(), os: 'Android', osv: subPick([ '16', '15', '14', '13' ]), model: subPick([ 'samsung SM-S938B', 'samsung SM-A546E', 'samsung SM-A556E', 'Xiaomi 24116RACCG', 'Xiaomi 2311DRK48G' ]) }; } },
{ id: 'incy_ios', label: 'INCY · iOS', make: function() { var v = subPick([ [ 'INCY/2.6.4/ios CFNetwork/3896.100.1.2.1 Darwin/27.0.0', '27.0.1' ], [ 'INCY/2.6.4/ios CFNetwork/3860.700.2 Darwin/25.6.0', '26.6.2' ] ]); return { ua: v[0], hwid: subIncyId(), os: 'iOS', osv: v[1], model: subPick(SUB_CLIENT_IPHONES) }; } }
];
var SEC_EXT = { sub_fmt: 'auto', sub_priority: false, ping_auto: false, sub_group: 'off', sub_plen: '2', sub_incl: '', sub_insecure: false, sub_ua: '', sub_hwid: '', sub_os: '', sub_osv: '', sub_model: '', ut_iv: '3m', ut_tol: '50', ut_url: UT_URL_DEF, ut_guard: false, direct_fallback: false,
uot: false, mixed: false, mixed_port: '2080', real_ip: false, dres_on: false, dres_type: 'udp', dres_server: '' }; uot: false, mixed: false, mixed_port: '2080', real_ip: false, dres_on: false, dres_type: 'udp', dres_server: '' };
var SET_EXT = { dns_ttl: '60', dns_ecs: '', src_ifs: [ 'br-lan' ], out_if: '', badwan: false, badwan_ifs: [], badwan_delay: '2000', ipv6: false, no_dhcp: false, var SET_EXT = { dns_ttl: '60', dns_ecs: '', src_ifs: [ 'br-lan' ], out_if: '', badwan: false, badwan_ifs: [], badwan_delay: '2000', ipv6: false, no_dhcp: false,
excl_ntp: false, excl_bt: false, bt_ports: '', block_doh: false, log_level: 'panic', yacd: false, yacd_wan: false, yacd_secret: '', excl_ntp: false, wifi_call: false, excl_bt: false, bt_ports: '', block_doh: false, log_level: 'panic', yacd: false, yacd_wan: false, yacd_secret: '',
kill: false, hijack: false, quic_all: false, ecs_auto: false, fwd: '', p_dt: '', p_du: '', p_pt: '', p_pu: '' }; kill: false, hijack: false, quic_all: false, ecs_auto: false, fwd: '', p_dt: '', p_du: '', p_pt: '', p_pu: '' };
var PORT_KEYS = [ [ 'p_dt', 'TCP напрямую' ], [ 'p_du', 'UDP напрямую' ], [ 'p_pt', 'TCP через VPN' ], [ 'p_pu', 'UDP через VPN' ] ]; var PORT_KEYS = [ [ 'p_dt', 'TCP напрямую' ], [ 'p_du', 'UDP напрямую' ], [ 'p_pt', 'TCP через VPN' ], [ 'p_pu', 'UDP через VPN' ] ];
@@ -46754,6 +46847,7 @@ return view.extend({
if (draft.sub_group !== 'off') p.push(draft.sub_group === 'country' ? 'группы по странам' : 'группы по имени'); if (draft.sub_group !== 'off') p.push(draft.sub_group === 'country' ? 'группы по странам' : 'группы по имени');
if (inclWords(draft.sub_incl).length) p.push('отбор по словам'); if (inclWords(draft.sub_incl).length) p.push('отбор по словам');
if (draft.sub_insecure) p.push('без проверки сертификата'); if (draft.sub_insecure) p.push('без проверки сертификата');
if (draft.sub_hwid.trim() || draft.sub_os.trim() || draft.sub_model.trim()) p.push('своё устройство');
} }
if (draft.mode !== 'iface') { if (draft.mode !== 'iface') {
if (draft.ut_iv !== '3m' || draft.ut_tol.trim() !== '50' || (draft.ut_url.trim() || UT_URL_DEF) !== UT_URL_DEF) p.push('свой автовыбор'); if (draft.ut_iv !== '3m' || draft.ut_tol.trim() !== '50' || (draft.ut_url.trim() || UT_URL_DEF) !== UT_URL_DEF) p.push('свой автовыбор');
@@ -46772,7 +46866,7 @@ return view.extend({
if (draft.mode === 'sub') { if (draft.mode === 'sub') {
box.appendChild(E('h4', {}, 'Подписка')); box.appendChild(E('h4', {}, 'Подписка'));
box.appendChild(row('Формат', seg(SUB_FMT, draft.sub_fmt, function(v) { set('sub_fmt', v); }))); box.appendChild(row('Формат', seg(SUB_FMT, draft.sub_fmt, function(v) { set('sub_fmt', v); })));
box.appendChild(hint('Какой формат просить у сервиса первым. «Авто» подходит почти всем. Xray JSON (Happ) — если часть серверов (например, с XHTTP) сервис отдаёт только приложению Happ; sing-box — если сервис умеет отдавать его напрямую.')); box.appendChild(hint('Какой формат просить у сервиса первым. «Авто» подходит почти всем. Xray JSON (Happ) — если часть серверов (например, с XHTTP или Hysteria2) сервис отдаёт только приложению Happ; sing-box — если сервис умеет отдавать его напрямую. Когда подписку добавляют, панель один раз сравнивает форматы и ставит тот, где серверов больше; дальше формат сам не меняется — только вручную здесь.'));
box.appendChild(row('Группы', seg(SUB_GROUP, draft.sub_group, function(v) { set('sub_group', v); }))); box.appendChild(row('Группы', seg(SUB_GROUP, draft.sub_group, function(v) { set('sub_group', v); })));
if (draft.sub_group === 'prefix') box.appendChild(row('Символов', inp('sub_plen', '2', 70, 'первые символы имени, по которым серверы собираются в группу'))); if (draft.sub_group === 'prefix') box.appendChild(row('Символов', inp('sub_plen', '2', 70, 'первые символы имени, по которым серверы собираются в группу')));
box.appendChild(hint('Серверы собираются в группы, и в каждой работает автовыбор лучшего. Удобно, когда в подписке десятки серверов: в карточке «Серверы» выбираете страну, а не отдельный сервер.')); box.appendChild(hint('Серверы собираются в группы, и в каждой работает автовыбор лучшего. Удобно, когда в подписке десятки серверов: в карточке «Серверы» выбираете страну, а не отдельный сервер.'));
@@ -46780,6 +46874,26 @@ return view.extend({
box.appendChild(hint('Из подписки останутся только серверы, в имени которых есть хоть одно из слов (через |, регистр не важен). Убрать отдельные серверы удобнее маркерами в карточке «Серверы».')); box.appendChild(hint('Из подписки останутся только серверы, в имени которых есть хоть одно из слов (через |, регистр не важен). Убрать отдельные серверы удобнее маркерами в карточке «Серверы».'));
box.appendChild(row('User-Agent', inp('sub_ua', 'подбирается сам', 340))); box.appendChild(row('User-Agent', inp('sub_ua', 'подбирается сам', 340)));
box.appendChild(hint('Нужен редко: если сервис отдаёт подписку только определённому приложению, например Happ/2.0.')); box.appendChild(hint('Нужен редко: если сервис отдаёт подписку только определённому приложению, например Happ/2.0.'));
if (CAPS.sub_id) {
var devOn = !!(draft.sub_hwid.trim() || draft.sub_os.trim() || draft.sub_osv.trim() || draft.sub_model.trim());
box.appendChild(fold('subdev', 'Устройство для подписки', devOn ? (draft.sub_os.trim() || 'своё') + (draft.sub_model.trim() ? ' · ' + draft.sub_model.trim() : '') : 'роутер', function(fb) {
fb.appendChild(hint('Некоторые панели (например, Remnawave с лимитом устройств) отдают подписку только приложению на привязанном устройстве и проверяют, что оно о себе сообщает: HWID, систему и модель. Без этого роутер шлёт свои данные и занимает ещё одно место в лимите — или получает пустую подписку.', 0));
var cl = E('select', { 'class': 'cbi-input-select', 'style': 'max-width:100%' }, SUB_CLIENTS.map(function(c) { return E('option', { 'value': c.id }, c.label); }));
fb.appendChild(row('Как приложение', E('span', { 'style': 'display:inline-flex; gap:8px; flex-wrap:wrap; align-items:center' }, [ cl, E('button', { 'class': 'btn cbi-button', 'click': function(ev) {
ev.preventDefault();
var c = SUB_CLIENTS.filter(function(x) { return x.id === cl.value; })[0], v = c && c.make();
if (!v) return;
draft.sub_ua = v.ua; draft.sub_hwid = v.hwid; draft.sub_os = v.os; draft.sub_osv = v.osv; draft.sub_model = v.model;
touch(); renderPanes();
zm.toast('Заполнено как ' + c.label + ' — HWID новый. Если подписка привязана к телефону, лучше вставьте HWID из приложения', 'info');
} }, 'Заполнить') ])));
fb.appendChild(row('HWID', inp('sub_hwid', 'свой у роутера', 340)));
fb.appendChild(row('Система', inp('sub_os', 'OpenWrt Linux', 200)));
fb.appendChild(row('Версия системы', inp('sub_osv', 'версия ядра роутера', 160)));
fb.appendChild(row('Модель', inp('sub_model', 'модель роутера', 260)));
fb.appendChild(hint('HWID лучше скопировать из приложения на устройстве, где подписка уже работает (в Happ — «Настройки» → «HWID»): так роутер не займёт новое место в лимите. User-Agent выше заполняется вместе с остальным. Все поля пустые — роутер шлёт свои данные, как раньше.'));
}));
}
box.appendChild(sw(draft.sub_insecure, 'Не проверять сертификат подписки', 'Только для панелей по IP-адресу с самоподписанным сертификатом. Небезопасно: подписку могут подменить по дороге.', function() { set('sub_insecure', !draft.sub_insecure); })); box.appendChild(sw(draft.sub_insecure, 'Не проверять сертификат подписки', 'Только для панелей по IP-адресу с самоподписанным сертификатом. Небезопасно: подписку могут подменить по дороге.', function() { set('sub_insecure', !draft.sub_insecure); }));
} }
if (draft.mode !== 'iface') { if (draft.mode !== 'iface') {
@@ -47967,6 +48081,7 @@ return view.extend({
miscCard.appendChild(row('Порт торрент-клиента', inp('bt_ports', '51413', 160, 'необязательно'))); miscCard.appendChild(row('Порт торрент-клиента', inp('bt_ports', '51413', 160, 'необязательно')));
miscCard.appendChild(hint('Зашифрованные соединения и DHT по протоколу не распознать — укажите порт из настроек клиента (qBittorrent: «Соединение»), и они тоже пойдут мимо VPN.', 0)); miscCard.appendChild(hint('Зашифрованные соединения и DHT по протоколу не распознать — укажите порт из настроек клиента (qBittorrent: «Соединение»), и они тоже пойдут мимо VPN.', 0));
} }
if (CAPS.wifi_call) miscCard.appendChild(sw(draft.wifi_call, 'Звонки по Wi‑Fi — напрямую', 'Wi‑Fi Calling (VoWiFi) у операторов связи: телефон звонит через интернет по туннелю к оператору (порты UDP 500 и 4500). Через VPN оператор такие звонки часто не принимает — с этим переключателем они идут напрямую.', function() { set('wifi_call', !draft.wifi_call); }));
miscCard.appendChild(sw(draft.excl_ntp, 'Синхронизация времени — напрямую', 'Запросы NTP не пойдут через VPN: часы устройств сверяются с ближайшим сервером времени.', function() { set('excl_ntp', !draft.excl_ntp); })); miscCard.appendChild(sw(draft.excl_ntp, 'Синхронизация времени — напрямую', 'Запросы NTP не пойдут через VPN: часы устройств сверяются с ближайшим сервером времени.', function() { set('excl_ntp', !draft.excl_ntp); }));
if (CAPS.ports) { if (CAPS.ports) {
var psum = [], pd = portsShort(draft.p_dt, draft.p_du), pv = portsShort(draft.p_pt, draft.p_pu); var psum = [], pd = portsShort(draft.p_dt, draft.p_du), pv = portsShort(draft.p_pt, draft.p_pu);