b315001288
Признак на странице конфигурации отвечал только на вопрос «достучится ли админка», поэтому 0.0.0.0 показывался как норма — хотя внутренний control plane при нём опубликован на всех интерфейсах, а оркестратор такой конфигурации не создаёт. Состояний теперь четыре: канон профиля, wildcard, не-канонический loopback и недостижимый адрес. Backend не тронут: он по-прежнему отвечает только на вопрос достижимости — превращать лишнюю публикацию в отказ обслуживания значило бы отключить всех пиров. Исправлено ложное утверждение в его комментарии: пустой хост `:36712` в Go означает все интерфейсы, а не loopback. Удалены мёртвые фразы common.wait/enableSuccess/disableSuccess — остатки операций запуска, остановки и смены версии Hysteria, которых у панели нет.
97 lines
4.0 KiB
Go
97 lines
4.0 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},
|
||
{"пробелы по краям", " 127.0.0.1:36712 ", 36712},
|
||
{"другой адрес loopback", "127.0.0.5:36712", 36712},
|
||
// Wildcard включает loopback: обмен состоится, и отказывать в
|
||
// обслуживании из-за лишней публикации внутреннего API нельзя —
|
||
// это отключило бы всех пиров. Запрет на wildcard принадлежит
|
||
// оркестратору, который конфиг и создаёт, а сообщает о нём панель.
|
||
{"wildcard", "0.0.0.0:36712", 36712},
|
||
// Пустой хост — тот же wildcard, а не loopback: в Go `:36712`
|
||
// означает все интерфейсы.
|
||
{"хост не задан", ":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())
|
||
}
|
||
}
|