fix(admin): различать достижимость Traffic Stats API и соответствие профилю

Признак на странице конфигурации отвечал только на вопрос «достучится ли
админка», поэтому 0.0.0.0 показывался как норма — хотя внутренний control plane
при нём опубликован на всех интерфейсах, а оркестратор такой конфигурации не
создаёт. Состояний теперь четыре: канон профиля, wildcard, не-канонический
loopback и недостижимый адрес.

Backend не тронут: он по-прежнему отвечает только на вопрос достижимости —
превращать лишнюю публикацию в отказ обслуживания значило бы отключить всех
пиров. Исправлено ложное утверждение в его комментарии: пустой хост `:36712` в
Go означает все интерфейсы, а не loopback.

Удалены мёртвые фразы common.wait/enableSuccess/disableSuccess — остатки
операций запуска, остановки и смены версии Hysteria, которых у панели нет.
This commit is contained in:
2026-09-03 03:39:42 +05:00
parent cb20d8d28f
commit b315001288
10 changed files with 204 additions and 30 deletions
+13 -4
View File
@@ -135,23 +135,32 @@ func GetHysteria2ApiPort() (int64, error) {
// это и есть тот самый второй контракт: значение, которое админка приняла и
// проигнорировала.
//
// Вопрос здесь ровно один: ДОСТУЧИТСЯ ли админка по этому адресу. Соответствие
// production-профилю — вопрос другой и решается там, где конфиг создаётся
// (validateRuntimeConfig) и где он показывается оператору (страница
// конфигурации отдельно называет wildcard и не-канонический loopback).
//
// Что принимается:
//
// ":36712" -> хост не задан, Hysteria слушает loopback;
// "127.0.0.1:36712" -> канонический production-профиль;
// "127.0.0.5:36712" -> любой loopback достижим по loopback;
// "0.0.0.0:36712" -> wildcard включает loopback, обмен состоится.
// "0.0.0.0:36712" -> wildcard включает loopback, обмен состоится;
// ":36712" -> тот же wildcard: пустой хост в Go означает ВСЕ
// интерфейсы, а не loopback.
//
// Wildcard принимается именно потому, что связь между компонентами при нём
// РАБОТАЕТ: отказывать в обслуживании из-за лишней публикации внутреннего API
// значило бы наказывать за неё отключением всех пиров. Запрет на wildcard —
// решение оркестратора, который конфиг создаёт (см. validateRuntimeConfig).
// значило бы наказывать за неё отключением всех пиров. Сама лишняя публикация
// при этом не остаётся незамеченной — о ней сообщает панель.
func parseTrafficStatsPort(listen string) (int64, error) {
trimmed := strings.TrimSpace(listen)
if trimmed == "" {
return 0, errors.New("empty listen")
}
// Пустой хост подставляется только затем, чтобы SplitHostPort принял
// строку: сам по себе `:36712` означает все интерфейсы, и loopback среди
// них, поэтому подстановка не меняет ответа на вопрос о достижимости.
hostPort := trimmed
if strings.HasPrefix(trimmed, ":") {
hostPort = "127.0.0.1" + trimmed
+4 -2
View File
@@ -24,14 +24,16 @@ func TestParseTrafficStatsPortAcceptsReachableAddresses(t *testing.T) {
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},
// Пустой хост — тот же wildcard, а не loopback: в Go `:36712`
// означает все интерфейсы.
{"хост не задан", ":36712", 36712},
}
for _, tc := range cases {