a1c74caa0c
Поиск с флагом -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 на весь вызов;
- возврат пайплайна запрещён отдельной приёмкой.
346 lines
14 KiB
Bash
346 lines
14 KiB
Bash
#!/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
|
||
}
|