fix(install): сделать границу «хост изменён» настоящим инвариантом

fatal_pre_apply мог означать «хост уже изменён». install-state.json пишется
сразу после успешного preflight, до установки пакетов, но классификация
отказа его не учитывала. Падение apt-get объявлялось как «на сервере ничего
не изменено»: откат и обработка состояния пропускались, а маркер оставался на
диске и ломал следующую установку по clean-host контракту.

Ownership-флаги переформулированы с «шаг успешно завершился» на «операция
могла начать менять систему» и взводятся перед мутирующим вызовом: apt-get
умеет изменить систему и упасть. fatal_pre_apply теперь недостижим ни при
одном взведённом флаге, включая stateWritten.

Read-only guard PHASE 0 можно было обойти. Guard стоял на writeText,
writeTextAtomic, runVisible, runHidden и runRawVisible, но не на универсальном
run, через который в коде проходили и наблюдение (ss, systemctl is-active), и
настоящие мутации (useradd, install -d, mkdir, cp -a, tar). Универсального
раннера больше нет: runReadOnly/runReadOnlySecret без guard'а и runMutating*
под guard'ом, выбор — явное решение на месте вызова.

clean-host не замечал часть того, что удаляет purge. /var/lib/hysteria с
ACME-состоянием Hysteria, /var/log/hy2xs, /usr/local/lib/hy2xs и
/usr/local/bin/hy2xs-orchestrator не были маркерами: сервер, где остался
только старый runtime-state Hysteria, проходил проверку и получал свежую
установку поверх чужого состояния. Пути, которые install.sh создаёт между
фазами, помечены как созданные установщиком, иначе PHASE 1 отказала бы на
собственном оркестраторе.

purge-v0.sh --keep-hysteria-binary противоречил установщику: скрипт сохранял
/usr/local/bin/hysteria и сообщал «хост чист для установки HY2XS v1», хотя
clean-host считает этот бинарник legacy-маркером. Флаг удалён.

DNS проверялся на существование A-записи, но не на то, куда она ведёт. После
принудительной смены IPv4 провайдером doctor отвечал успехом, хотя клиентская
ссылка отправляла людей на чужую машину. Проверялся при этом HY2XS_DOMAIN,
тогда как в hysteria2:// уезжает HY2XS_PUBLIC_HOST.

Добавлен инвариант публичного endpoint: A-записи обязаны принадлежать
множеству публичных IPv4, назначенных интерфейсам этого сервера. Проверка
живёт в общем preflight, поэтому действует в install, reconfigure и doctor.
Адрес определяется локально, без внешних сервисов определения IP. Строгость
управляется HY2XS_PUBLIC_ENDPOINT_POLICY (strict по умолчанию); отсутствие
A-записи фатально при любом значении.

TS-санитайзер приведён к той же формулировке, что и Go: URL-значение
определяется по самому значению, а не по имени ключа.
This commit is contained in:
2026-08-27 20:49:38 +05:00
parent b52fac1394
commit a88268b0cd
37 changed files with 1161 additions and 293 deletions
+17 -16
View File
@@ -21,20 +21,28 @@ set -euo pipefail
APPLY="false"
CONFIRMED="false"
KEEP_HYSTERIA_BINARY="false"
log() { printf '[hy2xs-purge] %s\n' "$*"; }
warn() { printf '[hy2xs-purge] WARNING: %s\n' "$*" >&2; }
fail() { printf '[hy2xs-purge] ERROR: %s\n' "$*" >&2; exit 1; }
# Флага «оставить бинарник Hysteria» здесь нет намеренно.
#
# /usr/local/bin/hysteria входит в список legacy-маркеров clean-host
# контракта установщика. Скрипт, который оставлял бы его и при этом печатал
# «хост чист для установки HY2XS v1», прямо противоречил бы следующему
# запуску install.sh: тот отказался бы ставиться.
#
# Полезного сценария у флага тоже не было: свежая установка всё равно кладёт
# собственную проверенную по SHA-256 версию Hysteria.
usage() {
cat >&2 <<'EOF'
Usage: purge-v0.sh [--apply --yes-i-know] [--keep-hysteria-binary]
Usage: purge-v0.sh [--apply --yes-i-know]
(без флагов) показать план очистки и выйти
--apply выполнить очистку
--yes-i-know подтверждение: обязателен вместе с --apply
--keep-hysteria-binary не удалять /usr/local/bin/hysteria
(без флагов) показать план очистки и выйти
--apply выполнить очистку
--yes-i-know подтверждение: обязателен вместе с --apply
Скрипт удаляет службы, приложение, конфигурацию и БАЗУ ДАННЫХ админки.
EOF
@@ -46,7 +54,6 @@ parse_args() {
case "$1" in
--apply) APPLY="true"; shift ;;
--yes-i-know) CONFIRMED="true"; shift ;;
--keep-hysteria-binary) KEEP_HYSTERIA_BINARY="true"; shift ;;
-h|--help) usage ;;
*) fail "unknown argument: $1" ;;
esac
@@ -120,11 +127,7 @@ show_plan() {
fi
done
if [ "$KEEP_HYSTERIA_BINARY" = "true" ]; then
log "/usr/local/bin/hysteria будет сохранён (--keep-hysteria-binary)"
else
log "будет удалён /usr/local/bin/hysteria"
fi
log "будет удалён /usr/local/bin/hysteria"
log "из /etc/nftables.conf будет убрана строка include для $NFT_FRAGMENT"
warn "БАЗА ДАННЫХ админки (/var/lib/hy2xs-admin) удаляется: пиры и их ссылки будут потеряны"
@@ -164,9 +167,7 @@ remove_paths() {
rm -rf "$path"
done
if [ "$KEEP_HYSTERIA_BINARY" != "true" ]; then
rm -f /usr/local/bin/hysteria
fi
rm -f /usr/local/bin/hysteria
}
# Из /etc/nftables.conf убирается только include HY2XS: остальной ruleset
@@ -202,7 +203,7 @@ verify_clean_host() {
[ -e "$path" ] && leftovers+=("$path")
done
if [ "$KEEP_HYSTERIA_BINARY" != "true" ] && [ -e /usr/local/bin/hysteria ]; then
if [ -e /usr/local/bin/hysteria ]; then
leftovers+=(/usr/local/bin/hysteria)
fi