fix(v1): сделать отзыв доступа, бэкап и диагностику соответствующими своим именам

Проход по операциям, которые делают не то, что обещает их имя.

P0. Удаление bootstrap-admin-peer не было отзывом доступа. Признаком «создавать
пир или нет» служило наличие строки в таблице, а HY2XS_ADMIN_CON_PASS
продолжает жить в /etc/hy2xs/hy2xs.env — его читает systemd-юнит. Оператор
удалял пира, доступ исчезал, и ближайший restart возвращал того же пира с тем же
секретом. Молча. Признаком стала отметка BOOTSTRAP_PEER_SEEDED в таблице config:
«создавался когда-либо», а не «существует сейчас». Отметка и пир пишутся одной
транзакцией.

P1. Резервная копия с includeSecrets=true проглатывала и ошибку расшифровки, и
отсутствие шифртекста, отдавая пира с пустым secret и успешный ответ. Теперь
недоступный секрет любого пира отклоняет весь запрос с указанием имени.

P1. DecryptPeerSecret возвращала содержимое колонки как расшифрованный секрет,
если оно не начиналось с v1: — остаток поколения с открытыми секретами.

P1. doctor перезапускал hysteria-server и hy2xs-admin: диагностика подозрения на
проблему обрывала все живые соединения.

P1. Админка сама генерировала HYSTERIA2_TRAFFIC_STATS_SECRET, записать который в
/etc/hysteria/config.yaml она не может. Сервис объявлял себя здоровым, а machine
auth переставал совпадать.

P1. Обходы проверки зависимостей (accepted-risk/skipped) не могли произвести
артефакт: приёмка требует dependency_security_gate=true. Удалены из сборки и
документации, отсутствие проверяется приёмкой.

P2. UPDATE по отсутствующей строке config считался успехом, и cron
перепланировался при несохранённом значении. Решение по RowsAffected.

P2. Слой данных не отличал «записи нет» от «база не ответила»: sentinel-значения
ErrPeerNotFound / ErrAdminUserNotFound / ErrConfigNotFound / ErrStorage.

P2. Удалены алиасы /:id/client-url и /:id/qr.

Контракт разработки: apps/go.mod объявляет toolchain go1.26.7 (директива go —
языковой baseline, а не выбор компилятора), tools/dev/doctor.sh|.ps1 сверяют
среду с versions.env.
This commit is contained in:
2026-08-30 06:48:50 +05:00
parent e30fdaa004
commit cf094f6e6f
33 changed files with 1976 additions and 154 deletions
+130
View File
@@ -0,0 +1,130 @@
#Requires -Version 7.0
<#
.SYNOPSIS
Проверка среды разработки против versions.env (Windows).
.DESCRIPTION
Точный аналог tools/dev/doctor.sh: сборка релиза скачивает Go, Node и Bun
ровно тех версий, что записаны в versions.env, а машина разработчика не
проверялась никак — расхождение обнаруживалось только на Debian, внутри
release-сборки.
Расхождение не гипотетическое: локальный Go 1.25 собирал проект успешно
(директива `go` в go.mod — языковой baseline, а не выбор компилятора), а
релизный бинарь компилировался на 1.26.7 и наследовал ЕЁ stdlib. То есть
проверялся не тот код, который уезжает в production.
Скрипт ничего не устанавливает и не меняет.
.PARAMETER Quiet
Печатать только итог.
#>
[CmdletBinding()]
param(
[switch]$Quiet
)
$ErrorActionPreference = 'Stop'
$rootDir = Resolve-Path (Join-Path $PSScriptRoot '..' '..')
Set-Location $rootDir
$versionsPath = Join-Path $rootDir 'versions.env'
if (-not (Test-Path $versionsPath)) {
Write-Error 'versions.env не найден: doctor запускается из репозитория HY2XS'
}
# versions.env — POSIX shell, но по построению содержит только присваивания
# KEY=value и комментарии: это его объявленный контракт (см. шапку файла).
$contract = @{}
foreach ($line in Get-Content $versionsPath) {
$trimmed = $line.Trim()
if (-not $trimmed -or $trimmed.StartsWith('#')) { continue }
$separator = $trimmed.IndexOf('=')
if ($separator -lt 1) { continue }
$contract[$trimmed.Substring(0, $separator).Trim()] = $trimmed.Substring($separator + 1).Trim()
}
$failures = 0
function Say([string]$Text) {
if (-not $Quiet) { Write-Host $Text }
}
# Версия спрашивается у самого бинаря: что записано в PATH — не тот вопрос,
# отвечать должен тот инструмент, который реально запустится.
function Get-ToolVersion {
param([string]$Command, [string[]]$Arguments)
$resolved = Get-Command $Command -ErrorAction SilentlyContinue
if (-not $resolved) { return 'not found' }
try {
$output = & $resolved.Source @Arguments 2>$null | Select-Object -First 1
} catch {
return 'unknown'
}
$match = [regex]::Match([string]$output, '\d+\.\d+\.\d+')
if (-not $match.Success) { return 'unknown' }
return $match.Value
}
function Test-Contract {
param([string]$Label, [string]$Required, [string]$Found, [string]$Hint)
if (-not $Found) { $Found = 'unknown' }
Say ''
Say "${Label}:"
Say " required: $Required"
Say " found: $Found"
if ($Found -eq $Required) {
Say ' OK'
return
}
Say " FAIL — $Hint"
$script:failures++
}
Say 'HY2XS development environment'
Say "contract: versions.env (HY2XS $($contract.HY2XS_VERSION), release line $($contract.HY2XS_RELEASE_LINE))"
# Go попадает в production-артефакт целиком: его stdlib компилируется в
# hy2xs-admin. Несовпадение здесь — не удобство, а разная поверхность атаки у
# разработчика и у релиза.
Test-Contract 'Go' $contract.GO_VERSION (Get-ToolVersion 'go' @('version')) `
"локальный Go собирает не ту stdlib, что уедет в релиз; поставьте $($contract.GO_VERSION)"
# Node живёт только на build-хосте, но собирает код, который уезжает в production.
Test-Contract 'Node' $contract.NODE_VERSION (Get-ToolVersion 'node' @('--version')) `
"frontend-ассеты собираются другой версией Node; поставьте $($contract.NODE_VERSION)"
Test-Contract 'pnpm' $contract.PNPM_VERSION (Get-ToolVersion 'pnpm' @('--version')) `
"разрешение зависимостей frontend может разойтись с lock-файлом; поставьте $($contract.PNPM_VERSION)"
# Bun физически входит в скомпилированный оркестратор (`bun build --compile`),
# поэтому его версия — часть артефакта, а не инструмента.
Test-Contract 'Bun' $contract.BUN_VERSION (Get-ToolVersion 'bun' @('--version')) `
"runtime Bun входит в бинарь оркестратора; поставьте $($contract.BUN_VERSION)"
# go.mod обязан называть компилятор явно: директива `go` этого не делает.
$declaredToolchain = (Select-String -Path (Join-Path $rootDir 'apps' 'go.mod') -Pattern '^toolchain\s+(go[0-9][^\s]*)' |
Select-Object -First 1).Matches.Groups[1].Value
if (-not $declaredToolchain) { $declaredToolchain = 'отсутствует' }
Test-Contract 'apps/go.mod toolchain' "go$($contract.GO_VERSION)" $declaredToolchain `
'директива toolchain разошлась с versions.env; сборка отвергнет это в verify_go_toolchain_contract'
Say ''
if ($failures -eq 0) {
Say 'Итог: среда разработки совпадает с контрактом сборки.'
exit 0
}
Say "Итог: расхождений — $failures. Локальные проверки идут не по тому toolchain,"
Say 'которым собирается релиз.'
exit 1
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
set -euo pipefail
# Проверка среды разработки против versions.env.
#
# Зачем.
#
# versions.env — единый контракт «продукт / платформа / toolchain», и сборка
# релиза его соблюдает: она скачивает Go, Node и Bun ровно тех версий, что
# записаны здесь, сверяя контрольные суммы. Машина разработчика при этом не
# проверялась НИКАК, поэтому расхождение обнаруживалось только на Debian, внутри
# release-сборки, и выглядело как «у меня работало».
#
# Расхождение не гипотетическое: локальный Go 1.25 собирал проект успешно (`go`
# в go.mod — языковой baseline, а не выбор компилятора), а релизный бинарь
# компилировался на 1.26.7 и наследовал ЕЁ stdlib. То есть проверялся не тот
# код, который уезжает в production.
#
# Скрипт ничего не устанавливает и не меняет: он отвечает на один вопрос —
# совпадает ли эта машина с контрактом сборки.
#
# Использование:
# ./tools/dev/doctor.sh проверить и вернуть код выхода
# ./tools/dev/doctor.sh --quiet только итог
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
QUIET=false
for arg in "$@"; do
case "$arg" in
--quiet) QUIET=true ;;
-h|--help)
printf 'usage: %s [--quiet]\n' "$0"
exit 0
;;
*)
printf 'unknown argument: %s\n' "$arg" >&2
exit 2
;;
esac
done
[ -f versions.env ] || {
printf 'versions.env не найден: doctor запускается из репозитория HY2XS\n' >&2
exit 2
}
# shellcheck disable=SC1091
. ./versions.env
FAILURES=0
say() {
[ "$QUIET" = true ] || printf '%s\n' "$1"
}
# Версия извлекается из вывода самого инструмента: спрашивать `которая версия
# записана в PATH` бессмысленно, отвечать должен тот бинарь, который реально
# запустится.
tool_version() {
local command="$1"
shift
command -v "$command" >/dev/null 2>&1 || {
printf 'not found'
return 0
}
"$command" "$@" 2>/dev/null | head -n1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n1
}
check() {
local label="$1" required="$2" found="$3" hint="$4"
if [ -z "$found" ]; then
found="unknown"
fi
say ""
say "$label:"
say " required: $required"
say " found: $found"
if [ "$found" = "$required" ]; then
say " OK"
return 0
fi
say " FAIL — $hint"
FAILURES=$((FAILURES + 1))
}
say "HY2XS development environment"
say "contract: versions.env (HY2XS $HY2XS_VERSION, release line $HY2XS_RELEASE_LINE)"
# Go попадает в production-артефакт целиком: его stdlib компилируется в
# hy2xs-admin. Несовпадение здесь — не удобство, а разная поверхность атаки у
# разработчика и у релиза.
check "Go" "$GO_VERSION" "$(tool_version go version)" \
"локальный Go собирает не ту stdlib, что уедет в релиз; поставьте $GO_VERSION"
# Node живёт только на build-хосте (сборка frontend-ассетов), но собирает код,
# который уезжает в production.
check "Node" "$NODE_VERSION" "$(tool_version node --version)" \
"frontend-ассеты собираются другой версией Node; поставьте $NODE_VERSION"
check "pnpm" "$PNPM_VERSION" "$(tool_version pnpm --version)" \
"разрешение зависимостей frontend может разойтись с lock-файлом; поставьте $PNPM_VERSION"
# Bun физически входит в скомпилированный оркестратор (`bun build --compile`),
# поэтому его версия — часть артефакта, а не инструмента.
check "Bun" "$BUN_VERSION" "$(tool_version bun --version)" \
"runtime Bun входит в бинарь оркестратора; поставьте $BUN_VERSION"
# go.mod обязан называть компилятор явно: директива `go` этого не делает.
DECLARED_TOOLCHAIN="$(grep -E '^toolchain[[:space:]]+go[0-9]' apps/go.mod | head -n1 | awk '{print $2}' || true)"
check "apps/go.mod toolchain" "go$GO_VERSION" "${DECLARED_TOOLCHAIN:-отсутствует}" \
"директива toolchain разошлась с versions.env; сборка отвергнет это в verify_go_toolchain_contract"
say ""
if [ "$FAILURES" -eq 0 ]; then
say "Итог: среда разработки совпадает с контрактом сборки."
exit 0
fi
say "Итог: расхождений — $FAILURES. Локальные проверки идут не по тому toolchain,"
say "которым собирается релиз."
exit 1