fix(admin): связать отзыв учётных данных с идентичностью сессий и свести адрес control plane к одному
Отзыв секрета не сходился: `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
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"hy2xs-admin/dao"
|
||||
"hy2xs-admin/model/constant"
|
||||
"hy2xs-admin/model/dto"
|
||||
"hy2xs-admin/model/vo"
|
||||
)
|
||||
|
||||
// Отзыв доступа проверяется ПРОТИВ НАСТОЯЩЕГО HTTP, а не против подменённого
|
||||
@@ -183,13 +184,13 @@ func pointHysteriaConfigAtDeadPort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// withHysteriaRunning подменяет ответ systemd о состоянии службы.
|
||||
func withHysteriaRunning(t *testing.T, running bool) {
|
||||
// withHysteriaServiceState подменяет ответ systemd о состоянии службы.
|
||||
func withHysteriaServiceState(t *testing.T, state HysteriaServiceState) {
|
||||
t.Helper()
|
||||
|
||||
previous := hysteria2IsRunning
|
||||
hysteria2IsRunning = func() bool { return running }
|
||||
t.Cleanup(func() { hysteria2IsRunning = previous })
|
||||
previous := hysteriaServiceState
|
||||
hysteriaServiceState = func() HysteriaServiceState { return state }
|
||||
t.Cleanup(func() { hysteriaServiceState = previous })
|
||||
}
|
||||
|
||||
func peerByName(t *testing.T, name string) (disabled int64, bannedUntil int64) {
|
||||
@@ -351,7 +352,7 @@ func TestDisconnectAuthIDsReportsApiFailure(t *testing.T) {
|
||||
func TestDisconnectAuthIDsIgnoresSystemdOpinion(t *testing.T) {
|
||||
newTestDB(t)
|
||||
stub := startTrafficStats(t, nil)
|
||||
withHysteriaRunning(t, false)
|
||||
withHysteriaServiceState(t, HysteriaServiceInactive)
|
||||
|
||||
if err := disconnectAuthIDs([]string{"alpha-auth-id"}); err != nil {
|
||||
t.Fatalf("разрыв сессии отказал из-за мнения systemd: %v", err)
|
||||
@@ -576,7 +577,7 @@ func TestHysteria2AuthRejectsWhenTrafficStatsUnreachable(t *testing.T) {
|
||||
func TestHysteria2AuthEnforcesLimitWhenSystemdSaysStopped(t *testing.T) {
|
||||
newTestDB(t)
|
||||
startTrafficStats(t, &trafficStatsStub{online: map[string]int64{"alpha-auth-id": 3}})
|
||||
withHysteriaRunning(t, false)
|
||||
withHysteriaServiceState(t, HysteriaServiceInactive)
|
||||
seedPeer(t, "alpha1", "alpha-auth-id")
|
||||
|
||||
if _, _, err := Hysteria2Auth("alpha1-secret"); err == nil {
|
||||
@@ -648,19 +649,79 @@ func TestHysteria2AuthRejectsPeerWithoutIdentity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Отображение остаётся терпимым: пустая картина подключений — честный ответ на
|
||||
// вопрос «кто сейчас на связи», когда служба остановлена. Строгость нужна
|
||||
// только там, где принимается решение о доступе.
|
||||
func TestHysteria2OnlineStaysTolerantForDisplay(t *testing.T) {
|
||||
// Путь отображения БОЛЬШЕ НЕ выдаёт «никто не подключён» за ответ.
|
||||
//
|
||||
// Прежний контракт был терпимым: при неактивной по мнению systemd службе
|
||||
// возвращалась пустая карта без ошибки. Выглядело это как честный ответ «сейчас
|
||||
// никого нет», а означало «спросить не удалось либо не спрашивали вовсе» —
|
||||
// ответ systemctl не отличает «служба неактивна» от «systemctl не запустился».
|
||||
//
|
||||
// Цена была не в формулировке. Сборщик метрик получал `err == nil` и выставлял
|
||||
// `apiReachable = true`, ни разу не обратившись к Traffic Stats API, поэтому
|
||||
// дашборд утверждал одновременно «Hysteria остановлена» и «API доступен, онлайн
|
||||
// 0». Список пиров по тому же ответу показывал всех офлайн.
|
||||
//
|
||||
// Теперь недоступность остаётся ошибкой, а как показать её оператору, решает
|
||||
// вызывающий — см. TestPagePeerReportsUnknownOnlineState.
|
||||
func TestHysteria2OnlineReportsFailureInsteadOfEmptyPicture(t *testing.T) {
|
||||
newTestDB(t)
|
||||
pointHysteriaConfigAtDeadPort(t)
|
||||
withHysteriaRunning(t, false)
|
||||
withHysteriaServiceState(t, HysteriaServiceInactive)
|
||||
|
||||
if _, err := Hysteria2Online(); err == nil {
|
||||
t.Fatal("недоступность Traffic Stats API выдана за пустую картину подключений")
|
||||
}
|
||||
}
|
||||
|
||||
// Мнение systemd на картину подключений не влияет вовсе: спрашивают того, кто
|
||||
// знает ответ.
|
||||
func TestHysteria2OnlineIgnoresSystemdOpinion(t *testing.T) {
|
||||
newTestDB(t)
|
||||
startTrafficStats(t, &trafficStatsStub{online: map[string]int64{"alpha-auth-id": 2}})
|
||||
withHysteriaServiceState(t, HysteriaServiceInactive)
|
||||
|
||||
online, err := Hysteria2Online()
|
||||
if err != nil {
|
||||
t.Fatalf("отображение подключений стало отказывать: %v", err)
|
||||
t.Fatalf("картина подключений отказала из-за мнения systemd: %v", err)
|
||||
}
|
||||
if len(online) != 0 {
|
||||
t.Fatalf("ожидалась пустая картина подключений, получено %v", online)
|
||||
if online["alpha-auth-id"] != 2 {
|
||||
t.Fatalf("мнение systemd подменило фактический ответ API: %v", online)
|
||||
}
|
||||
}
|
||||
|
||||
// Список пиров при недоступном API сообщает «состояние неизвестно», а не «все
|
||||
// офлайн»: это разные ответы, и первый в аварии ведёт оператора искать
|
||||
// проблему у пользователей.
|
||||
func TestPagePeerReportsUnknownOnlineState(t *testing.T) {
|
||||
newTestDB(t)
|
||||
pointHysteriaConfigAtDeadPort(t)
|
||||
seedPeer(t, "alpha1", "alpha-auth-id")
|
||||
|
||||
records, total, onlineState, err := PagePeer(dto.PeerPageDto{})
|
||||
if err != nil {
|
||||
t.Fatalf("список пиров отказал из-за недоступного Traffic Stats API: %v", err)
|
||||
}
|
||||
if total != 1 || len(records) != 1 {
|
||||
t.Fatalf("список пиров потерян: записей %d, всего %d", len(records), total)
|
||||
}
|
||||
if onlineState != vo.PeerOnlineStateUnavailable {
|
||||
t.Fatalf("состояние подключений объявлено как %q, ожидалось %q", onlineState, vo.PeerOnlineStateUnavailable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPagePeerReportsKnownOnlineState(t *testing.T) {
|
||||
newTestDB(t)
|
||||
startTrafficStats(t, &trafficStatsStub{online: map[string]int64{"alpha-auth-id": 2}})
|
||||
seedPeer(t, "alpha1", "alpha-auth-id")
|
||||
|
||||
records, _, onlineState, err := PagePeer(dto.PeerPageDto{})
|
||||
if err != nil {
|
||||
t.Fatalf("список пиров отказал: %v", err)
|
||||
}
|
||||
if onlineState != vo.PeerOnlineStateOk {
|
||||
t.Fatalf("состояние подключений объявлено как %q, ожидалось %q", onlineState, vo.PeerOnlineStateOk)
|
||||
}
|
||||
if len(records) != 1 || !records[0].Online || records[0].OnlineDevices != 2 {
|
||||
t.Fatalf("картина подключений не доехала до строки: %+v", records)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user