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
+31 -1
View File
@@ -543,7 +543,37 @@ describe("страница конфигурации Hysteria", () => {
const source = configPage();
expect(source).toContain("hysteria.trafficStatsMissing");
expect(source).toContain("trafficStatsIsLoopback");
expect(source).toContain("trafficStatsListenState");
});
// Достижимость и соответствие профилю — РАЗНЫЕ вопросы.
//
// Первая версия отвечала только на первый: `0.0.0.0` считался нормой, потому
// что через wildcard админка достучится. Но wildcard — не loopback:
// внутренний control plane при нём опубликован на всех интерфейсах, и при
// `HY2XS_FIREWALL_MODE=external|off` его не прикрывает ничто. Оркестратор
// такой конфигурации не создаёт, значит она появилась правкой руками — и
// диагностический экран обязан это назвать.
test("адрес Traffic Stats API различает достижимость и профиль", () => {
const source = vueCodeOf(configPage());
// Канон профиля ровно один, и он назван литералом.
expect(source).toContain('"127.0.0.1"');
// Пустой хост — тот же wildcard, что и 0.0.0.0: в Go `:36712` означает все
// интерфейсы, а не loopback.
expect(source).toMatch(/host === "" \|\| host === "0\.0\.0\.0"/);
for (const state of ["canonical", "wildcard", "nonCanonicalLoopback", "unreachable"]) {
expect(source).toContain(state);
}
// Три разные новости — три разные фразы, и ни одна не потеряна в словарях.
for (const dictionary of [ru, en] as const) {
const hysteria = (dictionary as any).hysteria ?? {};
expect(typeof hysteria.trafficStatsUnreachable).toBe("string");
expect(typeof hysteria.trafficStatsWildcard).toBe("string");
expect(typeof hysteria.trafficStatsNonCanonical).toBe("string");
}
});
// Read-only экран не имеет права быть щедрее санитизированной выгрузки того