Files
HY2XS_flamy/apps/service/config_traffic_stats_test.go
T
founder cb20d8d28f 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
2026-09-02 23:24:01 +05:00

95 lines
3.8 KiB
Go

package service
import (
"os"
"path/filepath"
"strings"
"testing"
"hy2xs-admin/model/constant"
)
// Адрес Traffic Stats API — межкомпонентный контракт, и админка обязана
// заметить его нарушение, а не работать «как будто там loopback».
//
// Ломается это одной строкой в `/etc/hysteria/config.yaml`: Hysteria начинает
// слушать другой адрес, админка продолжает стучаться в 127.0.0.1, и весь контур
// доступа выключается fail-closed — ни один пир не проходит авторизацию, потому
// что лимит устройств проверить нечем. Отказ обязан называть причину.
func TestParseTrafficStatsPortAcceptsReachableAddresses(t *testing.T) {
cases := []struct {
name string
listen string
want int64
}{
{"канонический профиль", "127.0.0.1:36712", 36712},
{"хост не задан", ":36712", 36712},
{"пробелы по краям", " 127.0.0.1:36712 ", 36712},
{"другой адрес loopback", "127.0.0.5:36712", 36712},
// Wildcard включает loopback: обмен состоится, и отказывать в
// обслуживании из-за лишней публикации внутреннего API нельзя —
// это отключило бы всех пиров. Запрет на wildcard принадлежит
// оркестратору, который конфиг и создаёт.
{"wildcard", "0.0.0.0:36712", 36712},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := parseTrafficStatsPort(tc.listen)
if err != nil {
t.Fatalf("адрес %q отклонён: %v", tc.listen, err)
}
if got != tc.want {
t.Fatalf("порт %d, ожидался %d", got, tc.want)
}
})
}
}
func TestParseTrafficStatsPortRejectsUnreachableAddresses(t *testing.T) {
cases := []struct {
name string
listen string
}{
{"адрес LAN", "192.168.1.10:36712"},
{"публичный адрес", "203.0.113.10:36712"},
{"имя хоста", "hysteria.internal:36712"},
{"пустая строка", ""},
{"порт вне диапазона", "127.0.0.1:70000"},
{"без порта", "127.0.0.1"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if _, err := parseTrafficStatsPort(tc.listen); err == nil {
t.Fatalf("недостижимый адрес %q принят", tc.listen)
}
})
}
}
// Отказ обязан называть и адрес, и способ починки: оператор, получивший
// «connection refused» без объяснения, ищет проблему в Hysteria.
func TestGetHysteria2ApiPortExplainsNonLoopbackListen(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
content := "listen: \":443\"\ntrafficStats:\n listen: \"192.168.1.10:36712\"\n secret: \"s\"\n"
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("не удалось записать тестовый конфиг: %v", err)
}
previous := constant.Hysteria2ConfigPath
constant.Hysteria2ConfigPath = path
t.Cleanup(func() { constant.Hysteria2ConfigPath = previous })
_, err := GetHysteria2ApiPort()
if err == nil {
t.Fatal("админка приняла адрес, по которому не может обратиться")
}
if !strings.Contains(err.Error(), "192.168.1.10") {
t.Errorf("сообщение не называет фактический адрес: %q", err.Error())
}
if !strings.Contains(err.Error(), "reconfigure") {
t.Errorf("сообщение не называет способ починки: %q", err.Error())
}
}