Автообход: близкие ±10 % сравниваются замером dpi-detector, итог — сумма счетов

- В фоновое сравнение идут стратегии не ниже 90 % от основного счёта лучшей; первыми —
  открывшие больше адресов неоткрывшихся сервисов, всего до шести. Итоговый счёт =
  основной + дополнительный, побеждает больший.
- Дополнительный замер — как в dpi-detector (core/tcp16_scanner.py): по одному
  соединению десять запросов HEAD с X-Pad по 4 КБ, SNI из списка или example.com, узел
  засчитывается, если соединение пережило все десять. Прежнее «скачать 64 КБ с голого
  адреса» у всех стратегий давало одинаковые 3 из 37. Узлы — весь список, 110.
- Живой туннель WARP в российской колонии разводится заново: Cloudflare со временем
  стал отвечать на прежний адрес из DME, а туннель держался, потому что жив.
- Задача считается живой, только пока в её журнале нет __DONE__: номер завершившегося
  подбора достался проверке гео-DNS из cron, и установщик отказывался ставиться.
- Установщик под set -e молча обрывался на kill уже завершившегося фонового сравнения.
This commit is contained in:
xyzmean committed 2026-09-24 00:39:01 +03:00
1 parent e22b573785
commit 6538d93d2e
1 file changed
+79 -45
+79 -45
View File
@@ -13,12 +13,16 @@ echo -e "\n${MAGENTA}Устанавливаем Zapret Manager для LuCI${NC}"
# Сравнение равных стратегий в фоне — не повод отказывать: оно снимается, а стратегию меняет только
# в самом конце.
if [ -f /tmp/zapret-manager-luci/redbtn_deep.pid ]; then
kill "$(cat /tmp/zapret-manager-luci/redbtn_deep.pid 2>/dev/null)" 2>/dev/null
pkill -f "qnum=8397" 2>/dev/null
nft delete table inet zm_rb_ztest >/dev/null 2>&1
# `|| true` обязательно: установщик идёт под set -e, и kill уже завершившегося процесса
# молча обрывал установку сразу после заголовка.
kill "$(cat /tmp/zapret-manager-luci/redbtn_deep.pid 2>/dev/null)" 2>/dev/null || true
pkill -f "qnum=8397" 2>/dev/null || true
nft delete table inet zm_rb_ztest >/dev/null 2>&1 || true
fi
for _zm_job in redbtn steer; do
if [ -f /tmp/zapret-manager-luci/$_zm_job.pid ] && kill -0 "$(cat /tmp/zapret-manager-luci/$_zm_job.pid 2>/dev/null)" 2>/dev/null; then
# Жив процесс и в журнале нет __DONE__: номер завершившейся задачи мог достаться другому.
if [ -f /tmp/zapret-manager-luci/$_zm_job.pid ] && kill -0 "$(cat /tmp/zapret-manager-luci/$_zm_job.pid 2>/dev/null)" 2>/dev/null &&
! grep -q '^__DONE__' /tmp/zapret-manager-luci/$_zm_job.log 2>/dev/null; then
echo -e "${YELLOW}Идёт автообход или операция Steer — дождитесь окончания и запустите установку снова${NC}"
exit 1
fi
@@ -142,11 +146,21 @@ _ensure_deps() {
}
# Задача жива, если жив её процесс И в журнале ещё нет __DONE__. Одного kill -0 мало: номер
# процесса завершившейся задачи система отдаёт следующему, и на тестовом роутере его получила
# плановая проверка гео-DNS из cron — подбор «шёл» спустя час после «Готово», а установщик
# отказывался ставиться.
_job_running() { # ИМЯ
local pid="$JOBS_DIR/$1.pid"
[ -f "$pid" ] && kill -0 "$(cat "$pid" 2>/dev/null)" 2>/dev/null || return 1
! grep -q '^__DONE__' "$JOBS_DIR/$1.log" 2>/dev/null
}
job_start() {
local name="$1"; shift
local log="$JOBS_DIR/$name.log"
local pid="$JOBS_DIR/$name.pid"
if [ -f "$pid" ] && kill -0 "$(cat "$pid" 2>/dev/null)" 2>/dev/null; then
if _job_running "$name"; then
printf '{"started":true,"job":"%s","already_running":true}\n' "$name"
return 0
fi
@@ -161,7 +175,7 @@ job_status() {
local pid="$JOBS_DIR/$name.pid"
local log="$JOBS_DIR/$name.log"
local running="false" done="false" rc=""
if [ -f "$pid" ] && kill -0 "$(cat "$pid" 2>/dev/null)" 2>/dev/null; then running="true"; fi
_job_running "$name" && running="true"
if [ -f "$log" ] && grep -q '^__DONE__' "$log"; then
done="true"
rc=$(grep '^__DONE__' "$log" | tail -1 | awk '{print $2}')
@@ -2221,7 +2235,7 @@ test_action() {
v|flowseal|v_flowseal|youtube|current) ;;
*) echo '{"error":"неизвестный режим теста"}'; return 1 ;;
esac
if [ -f "$JOBS_DIR/redbtn.pid" ] && kill -0 "$(cat "$JOBS_DIR/redbtn.pid" 2>/dev/null)" 2>/dev/null; then
if _job_alive redbtn; then
echo '{"error":"идёт автообход — дождитесь его окончания"}'
return 1
fi
@@ -4756,7 +4770,7 @@ _rb_svc_names() { local id; for id in $1; do printf '%s, ' "$(_rb_svc_field "$id
# Сервис можно пустить через WARP, только если у него есть списки доменов или адресов.
_rb_routable() { [ -n "$(_rb_svc_field "$1" 3)$(_rb_svc_field "$1" 4)" ]; }
_rb_in() { grep -qxF "$1" "$2" 2>/dev/null; }
_job_alive() { [ -f "$JOBS_DIR/$1.pid" ] && kill -0 "$(cat "$JOBS_DIR/$1.pid" 2>/dev/null)" 2>/dev/null; }
_job_alive() { _job_running "$1"; }
# Наш объект в ubus должен пережить установку пакетов: luci-proto-amneziawg в post-install
# зовёт `rpcd reload`, следом идёт перезапуск сети, и rpcd поднимался без объекта
@@ -5332,7 +5346,10 @@ _st_warp_up() { # [repick]
# Живой туннель с прошлого раза не трогаем — даже если колония совпала с соседом: её
# выбрала разведка, когда другой не нашлось, и пересканировать его на каждом «Применить»
# значило бы рвать соединения ради того же результата. Развести заново — «Сменить точки входа».
if [ "$repick" != repick ] && _st_warp_alive_if "$i" && c="$(_st_colo_of "$i")"; then
# КРОМЕ РОССИЙСКОЙ: её разведка берёт последним запасом, когда не нашлось ничего, и
# держаться за неё только потому, что туннель жив, — значит оставить геоблок навсегда
# (на тестовом роутере так и вышло: HEL, DME, HEL). Такой туннель разводится заново.
if [ "$repick" != repick ] && _st_warp_alive_if "$i" && c="$(_st_colo_of "$i")" && ! _st_warp_is_ru "$c"; then
got="$(uci -q get "network.${i}_peer.endpoint_host") $(uci -q get "network.${i}_peer.endpoint_port") $c"
_rb_say "WARP $n работает: колония $c"
else
@@ -6375,20 +6392,23 @@ _rb_zt_check() { # ФАЙЛ_ЦЕЛЕЙ
rm -f "$okf"
}
# Углублённая проверка равных лучших — по узлам CDN из списка dpi-detector (Runnin4ik), как в
# brb-deep. Короткие запросы основного прохода не различают стратегии, набравшие одинаково, а
# российский DPI рвёт поток к CDN и хостингам на 16-20 КБ, когда страница уже «открылась». Здесь
# с каждого узла берётся диапазон в 64 КБ, и узел, отдавший меньше 20 КБ, считается оборванным.
# В списке есть узлы, которые столько не отдают и без DPI, поэтому числа сравниваются только
# между стратегиями на одном и том же наборе. Узел без имени — идём по адресу, сертификат не
# проверяется: важен объём дошедшего, а не подлинность узла.
# Дополнительный замер близких к лучшей — по узлам CDN из списка dpi-detector (Runnin4ik) и
# ТЕМ ЖЕ СПОСОБОМ, что у него (core/tcp16_scanner.py). Российский DPI рвёт соединение к CDN и
# хостингам, когда через него ушло 16-20 КБ, и страница, открывшаяся по короткой проверке, на
# этом месте замирает. Поэтому по ОДНОМУ соединению идут десять запросов HEAD с заголовком
# X-Pad по 4 КБ — около 40 КБ от нас, — и узел засчитывается, только если соединение пережило
# все десять. Качать к себе, как делал brb-deep, бесполезно: по голому адресу такие узлы
# столько не отдают, и у всех стратегий выходило одинаковое «3 из 37». Имя для SNI и Host — из
# списка, у кого оно есть, у остальных example.com, как у dpi-detector; сертификат не
# проверяется — важно, дожило ли соединение, а не чей узел.
RB_DEEP_URLS="https://raw.githubusercontent.com/Runnin4ik/dpi-detector/main/tcp16.json https://cdn.jsdelivr.net/gh/Runnin4ik/dpi-detector@main/tcp16.json"
RB_DEEP_MIN=20480
# Больше равных не проверяем: минута на стратегию, а дюжина равных — это уже четверть часа.
RB_DEEP_REQS=10
# Больше близких не проверяем: на роутере со 128 МБ это пять-шесть минут на стратегию.
RB_DEEP_MAX=6
# Цели: «id|адрес|порт», каждый третий узел списка — так в выборку попадают все провайдеры, а не
# только первые в файле (Hetzner и Cloudflare).
# Цели: «id|адрес|порт|имя», весь список — все сто с лишним узлов, как у dpi-detector. Замер
# идёт в фоне, когда обход уже работает, и лишние минуты тут дешевле выборки, в которую какой-то
# провайдер попал одним узлом.
_rb_zt_deep_targets() { # ФАЙЛ
local u src="$RB_RUN/zt.tcp16.json"
for u in $RB_DEEP_URLS; do
@@ -6396,25 +6416,31 @@ _rb_zt_deep_targets() { # ФАЙЛ
rm -f "$src"
done
[ -s "$src" ] || return 1
sed -n 's/.*"id":[[:space:]]*"\([^"]*\)".*"ip":[[:space:]]*"\([^"]*\)".*"port":[[:space:]]*\([0-9]*\).*/\1|\2|\3/p' "$src" |
awk 'NR % 3 == 1' > "$1"
awk 'function f(k, m) { if (match($0, "\"" k "\":[[:space:]]*\"?[^\",}]*")) { m = substr($0, RSTART, RLENGTH); sub(/^"[^"]*":[[:space:]]*"?/, "", m); return m } return "" }
/"ip"/ { print f("id") "|" f("ip") "|" f("port") "|" f("sni") }' "$src" > "$1"
rm -f "$src"
[ -s "$1" ]
}
_rb_zt_deep() { # ФАЙЛ_УЗЛОВ -> «необорванных всего»
local okf="$RB_RUN/zt.dok" run=0 id ip port sc total pids=""
_rb_zt_deep() { # ФАЙЛ_УЗЛОВ -> «выдержавших всего»
local okf="$RB_RUN/zt.dok" run=0 id ip port sni total pids="" pad k urls
: > "$okf"
total=$(grep -c '|' "$1")
while IFS='|' read -r id ip port; do
pad=$(head -c 4000 /dev/urandom | base64 2>/dev/null | tr -dc 'A-Za-z0-9' | head -c 4000)
[ -n "$pad" ] || pad=$(awk 'BEGIN { srand(); for (i = 0; i < 4000; i++) printf "%c", 97 + int(rand() * 26) }')
while IFS='|' read -r id ip port sni; do
[ -n "$ip" ] || continue
_rb_stopped && break
sc=https; [ "$port" = 80 ] && sc=http
(
n=$(curl -sk --local-port "$RB_ZT_LO-$RB_ZT_HI" --connect-timeout 4 --max-time 12 --range 0-65535 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) curl/8.0" -o /dev/null -w '%{size_download}' \
"$sc://$ip:$port/" 2>/dev/null)
[ "${n:-0}" -ge "$RB_DEEP_MIN" ] 2>/dev/null && echo "$id" >> "$okf"
if [ "$port" = 80 ]; then u="http://$ip/"; res=""
else sni="${sni:-example.com}"; u="https://$sni:$port/"; res="--resolve $sni:$port:$ip"; fi
urls=""; k=0
# -o у каждого адреса свой: без него заголовки второго и дальше шли в вывод.
while [ "$k" -lt "$RB_DEEP_REQS" ]; do urls="$urls -o /dev/null $u"; k=$((k + 1)); done
n=$(curl -sk -I $res --local-port "$RB_ZT_LO-$RB_ZT_HI" --connect-timeout 8 --max-time 12 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" \
-H "X-Pad: $pad" -w '%{http_code}\n' $urls 2>/dev/null | grep -c '^[1-5][0-9][0-9]$')
[ "${n:-0}" -ge "$RB_DEEP_REQS" ] 2>/dev/null && echo "$id" >> "$okf"
) &
pids="$pids $!"
run=$((run + 1))
@@ -6563,24 +6589,30 @@ _rb_svc_targets() { # "id id..." ФАЙЛ
done
}
_rb_zt_ties() { # ФАЙЛ_РЕЗУЛЬТАТОВ -> имена равных первому, не больше RB_DEEP_MAX
local key
key="$(tail -n +2 "$1" | head -n1)"; key="${key#* → }"
tail -n +2 "$1" | awk -v k="$key" '{ v = $0; sub(/^.* → /, "", v) } v == k { sub(/ → .*/, ""); print }' | head -n "$RB_DEEP_MAX"
# Близкие к лучшей: общий счёт не ниже 90 % от её счёта. Разница в пару целей на полсотни —
# это шум короткой проверки, а стратегия чуть ниже по общему счёту может открывать сервисы,
# которые у лучшей не открылись. Первой идёт лучшая (она применена), за ней остальные — сначала
# открывшие больше адресов неоткрывшихся сервисов; всего не больше RB_DEEP_MAX.
_rb_zt_ties() { # ФАЙЛ_РЕЗУЛЬТАТОВ -> имена, по строке
tail -n +2 "$1" | awk '
{ n = $0; sub(/ → .*/, "", n); g = $0; sub(/^.* → /, "", g); split(g, a, "/"); v = $0; sub(/^.* · сервисы /, "", v)
if (NR == 1) { top = a[1]; print "0\t" n; next }
if (a[1] * 10 >= top * 9) print (100000 - v) "\t" n }' |
sort -n | cut -f2- | head -n "$RB_DEEP_MAX"
}
# Равные лучшие — тот же общий счёт и те же адреса сервисов, что у первого. Если их больше
# одного, решает углублённая проверка: печатает имя победителя (или первого, если проверить не
# удалось или все набрали одинаково). Первый — применённый: другой побеждает, только набрав
# строго больше.
# Близкие к лучшей (см. _rb_zt_ties). Если их больше одной, у каждой меряется дополнительный
# счёт — узлы CDN, до которых дошёл поток, — и итоговый счёт = основной + дополнительный.
# Печатает имя победителя по итоговому (или первой, если проверить не удалось). Первая —
# применённая: другая побеждает, только набрав строго больше.
_rb_zt_deep_pick() { # ФАЙЛ_РЕЗУЛЬТАТОВ
local res="$1" best key ties n name blk="$RB_RUN/zt.dblock" tgt="$RB_RUN/zt.deep" r sc top=-1 win
local res="$1" best key ties n name blk="$RB_RUN/zt.dblock" tgt="$RB_RUN/zt.deep" r sc main tot top=-1 win
best=$(tail -n +2 "$res" | head -n1)
win="${best%% → *}"
ties="$(_rb_zt_ties "$res")"
n=$(printf '%s\n' "$ties" | grep -c .)
[ "$n" -gt 1 ] || { echo "$win"; return 0; }
echo "Сравниваем $n равные стратегии" >&2
echo "Сравниваем близкие стратегии: $n" >&2
_rb_zt_deep_targets "$tgt" || { echo " список для сравнения не скачался — оставляем применённую" >&2; echo "$win"; return 0; }
_rb_zt_rules_up || { echo "$win"; return 0; }
printf '%s\n' "$ties" > "$RB_RUN/zt.ties"
@@ -6591,8 +6623,10 @@ _rb_zt_deep_pick() { # ФАЙЛ_РЕЗУЛЬТАТОВ
r="$(_rb_zt_one "$blk" "$tgt" _rb_zt_deep)"
sc="${r%% *}"
[ "$sc" = -1 ] && { echo " не запустилась" >&2; continue; }
echo " $sc из ${r#* }" >&2
[ "$sc" -gt "$top" ] && { top="$sc"; win="$name"; }
main=$(awk -v n="$name" 'NR > 1 { m = $0; sub(/ → .*/, "", m); if (m == n) { sub(/^.* → /, ""); split($0, a, "/"); print a[1]; exit } }' "$res")
tot=$(( ${main:-0} + sc ))
echo " основной ${main:-0} + дополнительный $sc из ${r#* } = $tot" >&2
[ "$tot" -gt "$top" ] && { top="$tot"; win="$name"; }
done < "$RB_RUN/zt.ties"
_rb_zt_down
rm -f "$RB_RUN/zt.ties" "$blk" "$tgt"
@@ -7046,14 +7080,14 @@ do_redbtn_run() {
_rb_warn "DNS over HTTPS перехватывает DNS сети — сервисы через WARP работать не будут, нажмите «Исправить» на странице Steer"
fi
_rb_say "Готово, обход подобран"
# Равные лучшие сравниваются уже после — обход к этому моменту работает.
# Близкие к лучшей сравниваются уже после — обход к этому моменту работает.
if [ -n "$deep_cur" ] && [ -s "$RB_RUN/deep.res" ] && [ "$(_rb_zt_ties "$RB_RUN/deep.res" | grep -c .)" -gt 1 ]; then
# Правила проб снимаются ловушкой на выходе, а с ними и таблица изоляции — снимаем
# сейчас, чтобы ловушка не выдернула её из-под фоновой проверки.
_rb_probe_nft_down
trap - EXIT INT TERM
job_start redbtn_deep do_redbtn_deep "$deep_cur" >/dev/null
_rb_say "Лучшая стратегия определится позже — идёт сравнение равных"
_rb_say "Лучшая стратегия определится позже — идёт сравнение близких"
fi
}
@@ -8580,7 +8614,7 @@ return view.extend({
return;
}
if (!busy && data.steer_busy) heroCard.appendChild(E('div', { 'class': 'zm-ab-note zm-ab-note-warn' }, 'На странице Steer идёт операция — запуск станет доступен, когда она закончится.'));
if (!busy && data.deep) heroCard.appendChild(E('div', { 'class': 'zm-ab-note zm-st-note-ok' }, 'Лучшая стратегия определится позже — идёт сравнение равных.'));
if (!busy && data.deep) heroCard.appendChild(E('div', { 'class': 'zm-ab-note zm-st-note-ok' }, 'Лучшая стратегия определится позже — идёт сравнение близких.'));
else if (!busy && data.deep_note) heroCard.appendChild(E('div', { 'class': 'zm-ab-note zm-st-note-ok' }, data.deep_note));
if (busy) {