cb20d8d28f
Отзыв секрета не сходился: `auth_id` при смене секрета оставался прежним, поэтому сессия, установленная по отозванным учётным данным, была неотличима от законной, и цикл учёта не имел признака, по которому её следовало завершить. У состояния есть путь без единой неудачи — Hysteria регистрирует соединение в Traffic Stats API только после возврата backend-auth, поэтому успешный /kick может пройти мимо. Новое поколение credentials получает новый auth_id, kick идёт по старому, пережившая сессия становится orphan. Адрес Traffic Stats API имел два контракта: оркестратор принимал любой IPv4, админка всегда шла на loopback. Валидная по всем гейтам конфигурация выключала лимит устройств, учёт трафика и принудительное отключение разом. Адрес зафиксирован, а расхождение файла с ним админка называет. Состояние службы стало трёхзначным: util.Exec выбрасывал вывод systemctl при ненулевом коде, поэтому «остановлена» и «спросить не удалось» приходили одним значением, а доступность Traffic Stats API выводилась из него же. Журнал Hysteria разбирается в фактическом формате upstream (time — дробное число), страница конфигурации показывает файл вместо дефолтов UI и не возит секреты в браузер, санитайзер выгрузки следует по YAML-якорям. Разбор: docs/acceptance/2026-09-02-v1.0.0-rc4-preflight-findings.md
126 lines
7.1 KiB
Go
126 lines
7.1 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
"hy2xs-admin/util"
|
|
)
|
|
|
|
// Жизненный цикл Hysteria принадлежит systemd и install-оркестратору.
|
|
// Панель умеет только наблюдать состояние: функций Start/Stop/Restart и
|
|
// смены версии здесь нет намеренно, а не «временно отключены».
|
|
|
|
// InitHysteria2 проверяет единственное, что панели действительно нужно от
|
|
// Hysteria на старте: наличие бинарника, версию которого показывает дашборд и
|
|
// у которого спрашивает состояние HysteriaServiceState.
|
|
//
|
|
// Чтения HYSTERIA2_ENABLE здесь больше нет. Ключ был последним следом модели,
|
|
// в которой панель сама запускала и останавливала Hysteria: clean install
|
|
// создавал его со значением "0", записать "1" было некому, а вся ветка
|
|
// сводилась к строке в журнале. Настройка, которая ничем не управляет, но
|
|
// выглядит выключателем, хуже отсутствующей.
|
|
func InitHysteria2() error {
|
|
if !util.Exists(util.GetHysteria2BinPath()) {
|
|
return errors.New("systemd-managed hysteria binary not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Состояние службы имеет ТРИ значения, а не два.
|
|
//
|
|
// Что было. Ответ сводился к `bool`:
|
|
//
|
|
// func Hysteria2IsRunning() bool {
|
|
// _, err := util.Exec("systemctl is-active --quiet hysteria-server")
|
|
// return err == nil
|
|
// }
|
|
//
|
|
// `false` при этом означал сразу две несовместимые вещи — «служба неактивна» и
|
|
// «спросить не получилось», — потому что util.Exec выбрасывает вывод команды,
|
|
// как только код возврата не нулевой, а `is-active` отвечает именно кодом 3
|
|
// вместе со словом состояния в stdout. Прочитать это слово было нечем.
|
|
//
|
|
// Цена такой неопределённости оказалась не в одной неверной плашке. Пока
|
|
// «неизвестно» выглядело как «остановлена», на этом значении стояли решения:
|
|
// цикл учёта и проверка лимита устройств пропускались при сломанном systemctl
|
|
// и живой Hysteria. Оба гейта убраны (см. cron.go и Hysteria2Auth), и это
|
|
// правильно, но сама неопределённость никуда не делась — она осталась в
|
|
// диагностике, где как раз и вредна:
|
|
//
|
|
// Hysteria stopped
|
|
// Traffic Stats API reachable
|
|
// 0 online users
|
|
//
|
|
// Три утверждения об одной системе, из которых первое и второе не могут быть
|
|
// верны одновременно. Оператор, увидев такое в аварии, ищет не там.
|
|
//
|
|
// Теперь состояние читается словом, а не кодом возврата, и неизвестность
|
|
// называется неизвестностью.
|
|
type HysteriaServiceState string
|
|
|
|
const (
|
|
// HysteriaServiceActive — systemd сообщил "active".
|
|
HysteriaServiceActive HysteriaServiceState = "active"
|
|
// HysteriaServiceInactive — systemd назвал любое другое известное
|
|
// состояние: inactive, failed, activating, deactivating.
|
|
//
|
|
// Различать их дальше продукту незачем: панель отвечает на вопрос «идёт ли
|
|
// сейчас обслуживание», а не пересказывает конечный автомат systemd.
|
|
HysteriaServiceInactive HysteriaServiceState = "inactive"
|
|
// HysteriaServiceUnknown — спросить не удалось либо ответ не разобран.
|
|
//
|
|
// Это НЕ «служба остановлена». Единственное честное следствие — что о
|
|
// состоянии службы сейчас ничего не известно.
|
|
HysteriaServiceUnknown HysteriaServiceState = "unknown"
|
|
)
|
|
|
|
// Hysteria2ServiceState — ответ systemd о состоянии службы, и НЕ БОЛЬШЕ ТОГО.
|
|
//
|
|
// Значение годится для отображения и ни для чего больше. Решения о доступе
|
|
// принимаются по фактическому ответу Traffic Stats API — см. hysteria2Online и
|
|
// disconnectAuthIDs, — и это правило стоило продукту трёх дыр: гейт
|
|
// `if !Hysteria2IsRunning() { return }` в cron молча отключал и сбор трафика, и
|
|
// принудительное отключение при сломанном systemctl.
|
|
func Hysteria2ServiceState() HysteriaServiceState {
|
|
return hysteriaServiceState()
|
|
}
|
|
|
|
// hysteriaServiceState — та же проверка в виде значения.
|
|
//
|
|
// Переменная нужна тестам: без неё каждая проверка, зависящая от состояния
|
|
// службы, доказуема только на живом systemd, то есть не доказуема в сборке
|
|
// вовсе. Продуктовый путь от этого не меняется — значение по умолчанию и есть
|
|
// реализация.
|
|
var hysteriaServiceState = func() HysteriaServiceState {
|
|
// `--quiet` здесь недопустим: он подавляет ровно тот вывод, ради которого
|
|
// команда вызывается.
|
|
output, _, err := util.ExecProbe("systemctl is-active hysteria-server")
|
|
if err != nil {
|
|
return HysteriaServiceUnknown
|
|
}
|
|
return parseSystemdActiveState(output)
|
|
}
|
|
|
|
// parseSystemdActiveState разбирает ответ `systemctl is-active`.
|
|
//
|
|
// Слово состояния приезжает первой строкой. Неизвестное значение считается
|
|
// неизвестным состоянием, а не «неактивна»: список состояний systemd
|
|
// пополнялся и раньше, и молчаливое приведение нового слова к «остановлена»
|
|
// вернуло бы ровно ту ложь, от которой избавляет этот тип.
|
|
func parseSystemdActiveState(output string) HysteriaServiceState {
|
|
state := strings.TrimSpace(output)
|
|
if idx := strings.IndexAny(state, "\r\n"); idx >= 0 {
|
|
state = strings.TrimSpace(state[:idx])
|
|
}
|
|
|
|
switch state {
|
|
case "active":
|
|
return HysteriaServiceActive
|
|
case "inactive", "failed", "activating", "deactivating", "reloading", "maintenance":
|
|
return HysteriaServiceInactive
|
|
default:
|
|
return HysteriaServiceUnknown
|
|
}
|
|
}
|