Files
HY2XS_flamy/tools/build/lib/hysteria.sh
T
founder a1c74caa0c fix(build): исключить SIGPIPE из релизных гейтов под pipefail
Поиск с флагом -q прекращает чтение на первом совпадении и закрывает свой конец
канала. Продюсер, которому осталось что писать, получает SIGPIPE и завершается
кодом 141, а `set -o pipefail` делает 141 статусом всей конструкции:

    совпадение НАЙДЕНО -> продюсер оборван -> статус 141 -> «не найдено»

Для утвердительных проверок это ложный FAIL. Для отрицательных — «такой
конструкции в коде нет» — ложный PASS: запрещённая конструкция найдена, а гейт
зелёный. Отрицательными проверками закреплена половина инвариантов приёмки,
включая запрет обхода тестов и запрет `pnpm audit --prod`.

Порог резкий: пока вывод продюсера помещается в буфер канала (64 KiB на Linux),
он не блокируется и успевает завершиться раньше, чем потребитель начнёт читать.
Замер, 60 прогонов на размер: до 60 KiB — 0 отказов, ровно на 64 KiB — 58/60,
от 96 KiB — 60/60. То есть проверка выглядит исправной ровно до первого
источника крупнее буфера, а такие файлы в репозитории уже есть.

- 56 мест переведены на here-string: `grep -q PATTERN <<<"$content"`;
- продюсеры-команды (ss|awk, dpkg-query, /proc/cpuinfo, systemctl
  list-unit-files) сначала читаются в переменную;
- введён code_has: десять отрицательных сканов держались на `|| true` внутри
  code_without_comments, гасившем 141, — то есть на побочном эффекте
  подавления ошибок, а не на заявленном свойстве;
- несуществующий путь в скане больше не означает успех: `2>/dev/null || true`
  превращал опечатку в пустой вывод, а пустой вывод для проверки «этого в коде
  нет» — это PASS. Проверка явная, а не через set -e: в контексте `! code_has`
  bash отключает errexit на весь вызов;
- возврат пайплайна запрещён отдельной приёмкой.
2026-09-01 04:28:54 +05:00

346 lines
14 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
# Разрешение upstream-версии Hysteria и compatibility gate.
#
# Ключевой инвариант: "latest" вычисляется на build machine и замораживается
# в metadata пакета. Target-сервер никогда не обращается к moving "latest".
HYSTERIA_LOCK_FILE="tools/build/hysteria-lock.env"
HYSTERIA_WORK_DIR="tools/build/output/hysteria"
# stable — определить последнюю стабильную версию через upstream API (default);
# pinned — использовать закоммиченный tools/build/hysteria-lock.env без сети.
HYSTERIA_CHANNEL="${HYSTERIA_CHANNEL:-stable}"
# Явный пин конкретной версии вида v2.12.2. Побеждает автоматический выбор.
HYSTERIA_VERSION_OVERRIDE="${HYSTERIA_VERSION_OVERRIDE:-}"
# Обновлять ли tools/build/hysteria-lock.env резолвнутыми значениями.
HYSTERIA_WRITE_LOCK="${HYSTERIA_WRITE_LOCK:-false}"
# Compatibility gate обязателен для production-сборки.
HYSTERIA_COMPAT_GATE="${HYSTERIA_COMPAT_GATE:-true}"
# Сверка скачанного бинарника с upstream hashes.txt. Отключается только
# break-glass: без неё SHA-256 считается от уже скачанного файла и является
# trust-on-first-use, а не проверкой происхождения артефакта.
HYSTERIA_VERIFY_UPSTREAM_HASHES="${HYSTERIA_VERIFY_UPSTREAM_HASHES:-true}"
HYSTERIA_LINUX_AMD64_ASSET="hysteria-linux-amd64"
HYSTERIA_GATE_PORT="${HYSTERIA_GATE_PORT:-34443}"
HYSTERIA_GATE_STATS_PORT="${HYSTERIA_GATE_STATS_PORT:-34712}"
HYSTERIA_GATE_TIMEOUT_SECONDS="${HYSTERIA_GATE_TIMEOUT_SECONDS:-15}"
validate_hysteria_version_format() {
local value="$1"
grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' <<<"$value" \
|| fail "invalid Hysteria version format: $value (expected vX.Y.Z)"
}
validate_hysteria_sha256_format() {
local value="$1"
case "$value" in
replace-with-*|"")
fail "HYSTERIA_ARTIFACT_SHA256 must be a real release sha256"
;;
esac
grep -Eq '^[a-fA-F0-9]{64}$' <<<"$value" \
|| fail "HYSTERIA_ARTIFACT_SHA256 must be a 64-char hex SHA256, got: $value"
}
load_pinned_hysteria_lock() {
[ -f "$HYSTERIA_LOCK_FILE" ] || fail "missing Hysteria lock file: $HYSTERIA_LOCK_FILE"
# shellcheck disable=SC1090
. "$HYSTERIA_LOCK_FILE"
[ -n "${HYSTERIA_VERSION:-}" ] || fail "HYSTERIA_VERSION is required in $HYSTERIA_LOCK_FILE"
[ -n "${HYSTERIA_ARTIFACT_URL:-}" ] || fail "HYSTERIA_ARTIFACT_URL is required in $HYSTERIA_LOCK_FILE"
[ -n "${HYSTERIA_ARTIFACT_SHA256:-}" ] || fail "HYSTERIA_ARTIFACT_SHA256 is required in $HYSTERIA_LOCK_FILE"
validate_hysteria_version_format "$HYSTERIA_VERSION"
validate_hysteria_sha256_format "$HYSTERIA_ARTIFACT_SHA256"
HYSTERIA_RESOLUTION="pinned"
HYSTERIA_RESOLVED_AT="${HYSTERIA_RESOLVED_AT:-unknown}"
# Сумма в lock-файле уже была проверена против upstream hashes.txt в тот
# момент, когда lock записывался.
HYSTERIA_SHA_SOURCE="hy2xs-lock"
export HYSTERIA_VERSION HYSTERIA_ARTIFACT_URL HYSTERIA_ARTIFACT_SHA256
export HYSTERIA_RESOLUTION HYSTERIA_RESOLVED_AT HYSTERIA_SHA_SOURCE
}
resolve_hysteria_from_upstream() {
local resolver_args=(--channel stable)
if [ -n "$HYSTERIA_VERSION_OVERRIDE" ]; then
validate_hysteria_version_format "$HYSTERIA_VERSION_OVERRIDE"
resolver_args+=(--version "$HYSTERIA_VERSION_OVERRIDE")
log_info "Resolving pinned Hysteria version override: $HYSTERIA_VERSION_OVERRIDE"
else
log_info "Resolving latest stable Hysteria release from upstream"
fi
local resolver_output
resolver_output="$(cd orchestrator && "$BUN_BIN" run tools/resolve-hysteria.ts "${resolver_args[@]}")" \
|| fail "Hysteria release resolution failed"
local key value line
HYSTERIA_VERSION=""
HYSTERIA_TAG=""
HYSTERIA_ARTIFACT_URL=""
HYSTERIA_HASHES_URL=""
HYSTERIA_PUBLISHED_AT=""
HYSTERIA_RESOLUTION=""
while IFS= read -r line; do
[ -n "$line" ] || continue
key="${line%%=*}"
value="${line#*=}"
case "$key" in
HYSTERIA_VERSION) HYSTERIA_VERSION="$value" ;;
HYSTERIA_TAG) HYSTERIA_TAG="$value" ;;
HYSTERIA_ARTIFACT_URL) HYSTERIA_ARTIFACT_URL="$value" ;;
HYSTERIA_HASHES_URL) HYSTERIA_HASHES_URL="$value" ;;
HYSTERIA_PUBLISHED_AT) HYSTERIA_PUBLISHED_AT="$value" ;;
HYSTERIA_RESOLUTION) HYSTERIA_RESOLUTION="$value" ;;
*) fail "unexpected resolver output line: $line" ;;
esac
done <<<"$resolver_output"
[ -n "$HYSTERIA_VERSION" ] || fail "resolver did not return HYSTERIA_VERSION"
[ -n "$HYSTERIA_ARTIFACT_URL" ] || fail "resolver did not return HYSTERIA_ARTIFACT_URL"
[ -n "$HYSTERIA_HASHES_URL" ] || fail "resolver did not return HYSTERIA_HASHES_URL"
validate_hysteria_version_format "$HYSTERIA_VERSION"
HYSTERIA_RESOLVED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
log_info "Resolved Hysteria $HYSTERIA_VERSION (tag ${HYSTERIA_TAG:-unknown}, published ${HYSTERIA_PUBLISHED_AT:-unknown})"
export HYSTERIA_VERSION HYSTERIA_TAG HYSTERIA_ARTIFACT_URL HYSTERIA_HASHES_URL
export HYSTERIA_PUBLISHED_AT HYSTERIA_RESOLUTION HYSTERIA_RESOLVED_AT
}
# Ожидаемый digest берётся из upstream-ассета hashes.txt ДО того, как он
# попадёт в HY2XS lock. Иначе замороженная сумма подтверждает только то, что
# файл не изменился после скачивания.
resolve_expected_sha_from_upstream() {
[ "$HYSTERIA_VERIFY_UPSTREAM_HASHES" = "true" ] || {
log_info "WARNING: upstream hashes.txt verification is disabled (HYSTERIA_VERIFY_UPSTREAM_HASHES=$HYSTERIA_VERIFY_UPSTREAM_HASHES)"
return 0
}
[ -n "${HYSTERIA_HASHES_URL:-}" ] || fail "upstream hashes.txt URL is unknown"
mkdir -p "$HYSTERIA_WORK_DIR"
local hashes_file="$HYSTERIA_WORK_DIR/hashes.txt"
log_info "Downloading upstream checksums: $HYSTERIA_HASHES_URL"
rm -f "$hashes_file"
curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \
"$HYSTERIA_HASHES_URL" -o "$hashes_file" \
|| fail "failed to download upstream hashes.txt: $HYSTERIA_HASHES_URL"
[ -s "$hashes_file" ] || fail "downloaded upstream hashes.txt is empty"
local upstream_sha
upstream_sha="$(cd orchestrator && "$BUN_BIN" run tools/parse-upstream-hashes.ts \
--file "../$hashes_file" --asset "$HYSTERIA_LINUX_AMD64_ASSET")" \
|| fail "could not read the expected SHA-256 from upstream hashes.txt"
validate_hysteria_sha256_format "$upstream_sha"
HYSTERIA_ARTIFACT_SHA256="$upstream_sha"
HYSTERIA_SHA_SOURCE="upstream-hashes"
export HYSTERIA_ARTIFACT_SHA256 HYSTERIA_SHA_SOURCE
log_info "Expected Hysteria SHA-256 from upstream hashes.txt: $upstream_sha"
}
download_hysteria_artifact() {
mkdir -p "$HYSTERIA_WORK_DIR"
HYSTERIA_BINARY="$HYSTERIA_WORK_DIR/hysteria-linux-amd64"
log_info "Downloading $HYSTERIA_ARTIFACT_URL"
rm -f "$HYSTERIA_BINARY"
curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \
"$HYSTERIA_ARTIFACT_URL" -o "$HYSTERIA_BINARY" \
|| fail "failed to download Hysteria artifact: $HYSTERIA_ARTIFACT_URL"
[ -s "$HYSTERIA_BINARY" ] || fail "downloaded Hysteria artifact is empty"
local actual_sha
actual_sha="$(sha256sum "$HYSTERIA_BINARY" | awk '{print $1}')"
if [ -n "${HYSTERIA_ARTIFACT_SHA256:-}" ]; then
validate_hysteria_sha256_format "$HYSTERIA_ARTIFACT_SHA256"
[ "$actual_sha" = "$HYSTERIA_ARTIFACT_SHA256" ] \
|| fail "Hysteria artifact sha256 mismatch: expected $HYSTERIA_ARTIFACT_SHA256 (${HYSTERIA_SHA_SOURCE:-unknown source}), got $actual_sha"
log_info "Hysteria artifact verified against ${HYSTERIA_SHA_SOURCE:-unknown source}: $actual_sha"
else
# Сюда попадаем только при выключенной сверке с upstream: сумма
# фиксируется как локальный fingerprint пакета, без проверки происхождения.
HYSTERIA_ARTIFACT_SHA256="$actual_sha"
HYSTERIA_SHA_SOURCE="local-download"
log_info "Computed Hysteria artifact sha256 without upstream verification: $HYSTERIA_ARTIFACT_SHA256"
fi
export HYSTERIA_ARTIFACT_SHA256 HYSTERIA_BINARY HYSTERIA_SHA_SOURCE
chmod 0755 "$HYSTERIA_BINARY"
local reported_version
reported_version="$("$HYSTERIA_BINARY" version 2>/dev/null | grep -Eo 'v[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)"
[ -n "$reported_version" ] || fail "unable to read version from downloaded Hysteria binary"
[ "$reported_version" = "$HYSTERIA_VERSION" ] \
|| fail "downloaded Hysteria binary reports $reported_version, expected $HYSTERIA_VERSION"
log_info "Verified Hysteria binary: $HYSTERIA_VERSION ($HYSTERIA_ARTIFACT_SHA256)"
}
assert_gate_port_is_free() {
local port="$1"
local proto_flag="$2"
if command -v ss >/dev/null 2>&1; then
# Вывод собирается ДО поиска: `grep -q` обрывает чтение на первом совпадении,
# и под `pipefail` SIGPIPE у продюсера превратил бы найденный занятый порт в
# ненулевой статус, то есть в вывод «порт свободен».
local sockets
sockets="$(ss -H "$proto_flag" 2>/dev/null | awk '{print $4}')" || sockets=""
if grep -Eq "[:.]${port}$" <<<"$sockets"; then
fail "compatibility gate port $port is already in use; override HYSTERIA_GATE_PORT/HYSTERIA_GATE_STATS_PORT"
fi
fi
}
gate_generate_self_signed_cert() {
local dir="$1"
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-subj /CN=hy2xs-compat-gate \
-keyout "$dir/gate.key" -out "$dir/gate.crt" >/dev/null 2>&1 \
|| fail "compatibility gate could not generate a self-signed certificate"
}
gate_run_single_profile() {
local obfs_type="$1"
local gate_dir="$2"
local config_path="$gate_dir/config-$obfs_type.yaml"
local log_path="$gate_dir/hysteria-$obfs_type.log"
log_info "Compatibility gate: rendering canonical config (obfs=$obfs_type)"
(
cd orchestrator
"$BUN_BIN" run tools/render-canonical-config.ts \
--package-dir ../package \
--obfs "$obfs_type" \
--tls-mode file \
--cert "$gate_dir/gate.crt" \
--key "$gate_dir/gate.key" \
--port "$HYSTERIA_GATE_PORT" \
--traffic-stats-port "$HYSTERIA_GATE_STATS_PORT" \
--out "$config_path"
) || fail "BUILD FAILED: HY2XS could not render canonical config for obfs=$obfs_type"
grep -q "type: $obfs_type" "$config_path" \
|| fail "compatibility gate rendered a config without obfs type $obfs_type"
log_info "Compatibility gate: starting Hysteria $HYSTERIA_VERSION with generated config (obfs=$obfs_type)"
HYSTERIA_DISABLE_UPDATE_CHECK=1 "$HYSTERIA_BINARY" server -c "$config_path" >"$log_path" 2>&1 &
local gate_pid=$!
local ready="false"
local waited=0
while [ "$waited" -lt "$HYSTERIA_GATE_TIMEOUT_SECONDS" ]; do
if ! kill -0 "$gate_pid" 2>/dev/null; then
break
fi
if command -v ss >/dev/null 2>&1; then
local udp_sockets
udp_sockets="$(ss -H -lun 2>/dev/null | awk '{print $4}')" || udp_sockets=""
if grep -Eq "[:.]${HYSTERIA_GATE_PORT}$" <<<"$udp_sockets"; then
ready="true"
break
fi
fi
sleep 1
waited=$((waited + 1))
done
if [ "$ready" != "true" ]; then
kill "$gate_pid" 2>/dev/null || true
wait "$gate_pid" 2>/dev/null || true
log_info "--- Hysteria compatibility gate output (obfs=$obfs_type) ---"
cat "$log_path" >&2 || true
fail "BUILD FAILED: unsupported Hysteria stable $HYSTERIA_VERSION (obfs=$obfs_type config was rejected)"
fi
kill "$gate_pid" 2>/dev/null || true
wait "$gate_pid" 2>/dev/null || true
log_info "Compatibility gate passed for obfs=$obfs_type"
}
run_hysteria_compatibility_gate() {
if [ "$HYSTERIA_COMPAT_GATE" != "true" ]; then
log_info "WARNING: Hysteria compatibility gate is disabled (HYSTERIA_COMPAT_GATE=$HYSTERIA_COMPAT_GATE)"
return 0
fi
require_tool openssl
[ -x "${HYSTERIA_BINARY:-}" ] || fail "compatibility gate requires a verified Hysteria binary"
assert_gate_port_is_free "$HYSTERIA_GATE_PORT" "-lun"
assert_gate_port_is_free "$HYSTERIA_GATE_STATS_PORT" "-ltn"
local gate_dir
gate_dir="$(mktemp -d)"
# shellcheck disable=SC2064
trap "rm -rf '$gate_dir'" RETURN
gate_generate_self_signed_cert "$gate_dir"
gate_run_single_profile "gecko" "$gate_dir"
gate_run_single_profile "salamander" "$gate_dir"
log_info "Compatibility gate: Hysteria $HYSTERIA_VERSION accepts the HY2XS production config"
}
write_hysteria_lock_file() {
if [ "$HYSTERIA_WRITE_LOCK" != "true" ]; then
return 0
fi
log_info "Updating $HYSTERIA_LOCK_FILE with resolved values"
cat >"$HYSTERIA_LOCK_FILE" <<EOF
# Файл генерируется tools/build/lib/hysteria.sh.
# HYSTERIA_CHANNEL=pinned использует эти значения без обращения к сети.
HYSTERIA_VERSION=$HYSTERIA_VERSION
HYSTERIA_ARTIFACT_URL=$HYSTERIA_ARTIFACT_URL
HYSTERIA_ARTIFACT_SHA256=$HYSTERIA_ARTIFACT_SHA256
HYSTERIA_SHA_SOURCE=${HYSTERIA_SHA_SOURCE:-unknown}
HYSTERIA_RESOLUTION=$HYSTERIA_RESOLUTION
HYSTERIA_RESOLVED_AT=$HYSTERIA_RESOLVED_AT
EOF
}
resolve_and_verify_hysteria() {
case "$HYSTERIA_CHANNEL" in
stable)
resolve_hysteria_from_upstream
# Ожидаемая сумма приходит из upstream hashes.txt, а не из локального
# файла: проверяется происхождение артефакта, а не только целостность.
HYSTERIA_ARTIFACT_SHA256=""
HYSTERIA_SHA_SOURCE=""
resolve_expected_sha_from_upstream
;;
pinned)
log_info "Using pinned Hysteria lock file (offline): $HYSTERIA_LOCK_FILE"
load_pinned_hysteria_lock
;;
*)
fail "unsupported HYSTERIA_CHANNEL: $HYSTERIA_CHANNEL (use stable or pinned)"
;;
esac
download_hysteria_artifact
run_hysteria_compatibility_gate
write_hysteria_lock_file
}