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:
@@ -100,9 +100,9 @@ export default {
|
||||
save: "Save",
|
||||
update: "Update",
|
||||
downloadSuccess: "Download successful",
|
||||
wait: "The version is being changed, please wait a moment",
|
||||
enableSuccess: "Hysteria2 start successful",
|
||||
disableSuccess: "Hysteria2 stop successful",
|
||||
// `wait`, `enableSuccess` and `disableSuccess` are gone together with the
|
||||
// operations the panel does not have: starting, stopping and changing the
|
||||
// version of Hysteria. That lifecycle belongs to systemd and the orchestrator.
|
||||
success: "Success",
|
||||
refresh: "Refresh",
|
||||
yes: "Yes",
|
||||
@@ -335,7 +335,11 @@ export default {
|
||||
trafficStatsMissing: "Traffic Stats API is not configured",
|
||||
trafficStatsMissingHint:
|
||||
"Without the trafficStats section the device limit, traffic accounting and forced disconnect do not work: the panel reaches Hysteria only through this API.",
|
||||
trafficStatsNotLoopback:
|
||||
"the address is not loopback: the panel reaches the Traffic Stats API over 127.0.0.1 only",
|
||||
trafficStatsUnreachable:
|
||||
"the panel cannot reach this address: the Traffic Stats API is queried over loopback only. While the addresses differ, the device limit, traffic accounting and forced disconnect do not work",
|
||||
trafficStatsWildcard:
|
||||
"the API is reachable but listens on every interface. The HY2XS production profile uses 127.0.0.1: run hy2xs-orchestrator reconfigure",
|
||||
trafficStatsNonCanonical:
|
||||
"the API is reachable but the address differs from 127.0.0.1 — the orchestrator never produces such a configuration",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -100,9 +100,9 @@ export default {
|
||||
save: "Сохранить",
|
||||
update: "Обновить",
|
||||
downloadSuccess: "Загрузка завершена",
|
||||
wait: "Версия меняется, подождите",
|
||||
enableSuccess: "Hysteria2 запущена",
|
||||
disableSuccess: "Hysteria2 остановлена",
|
||||
// Фразы `wait`, `enableSuccess` и `disableSuccess` удалены вместе с
|
||||
// операциями, которых у панели нет: запуском, остановкой и сменой версии
|
||||
// Hysteria. Жизненный цикл принадлежит systemd и оркестратору.
|
||||
success: "Готово",
|
||||
refresh: "Обновить",
|
||||
yes: "Да",
|
||||
@@ -346,7 +346,14 @@ export default {
|
||||
trafficStatsMissing: "Traffic Stats API не настроен",
|
||||
trafficStatsMissingHint:
|
||||
"Без секции trafficStats не работают лимит устройств, учёт трафика и принудительное отключение: панель обращается к Hysteria только через этот API.",
|
||||
trafficStatsNotLoopback:
|
||||
"адрес не loopback: панель обращается к Traffic Stats API только по 127.0.0.1",
|
||||
// Три разные новости об одном поле, и объединять их нельзя: первая
|
||||
// означает, что доступ пиров уже не работает, вторая — что внутренний API
|
||||
// опубликован шире необходимого, третья — просто дрейф от профиля.
|
||||
trafficStatsUnreachable:
|
||||
"панель не может обратиться по этому адресу: Traffic Stats API запрашивается только по loopback. Пока адреса расходятся, лимит устройств, учёт трафика и принудительное отключение не работают",
|
||||
trafficStatsWildcard:
|
||||
"API доступен, но слушает все интерфейсы. Production-профиль HY2XS использует 127.0.0.1: выполните hy2xs-orchestrator reconfigure",
|
||||
trafficStatsNonCanonical:
|
||||
"API доступен, но адрес отличается от 127.0.0.1 — оркестратор такой конфигурации не создаёт",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -308,10 +308,25 @@
|
||||
<span :class="valueClass(profile.trafficStats.listen)">{{
|
||||
text(profile.trafficStats.listen)
|
||||
}}</span>
|
||||
<!--
|
||||
Недостижимость и «шире, чем нужно» — разные новости: первая
|
||||
означает, что доступ пиров уже не работает, вторая — что
|
||||
внутренний API опубликован лишний раз.
|
||||
-->
|
||||
<span
|
||||
v-if="!trafficStatsIsLoopback"
|
||||
v-if="trafficStatsListenState === 'unreachable'"
|
||||
class="value-error"
|
||||
>{{ $t("hysteria.trafficStatsNotLoopback") }}</span
|
||||
>{{ $t("hysteria.trafficStatsUnreachable") }}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="trafficStatsListenState === 'wildcard'"
|
||||
class="value-warning"
|
||||
>{{ $t("hysteria.trafficStatsWildcard") }}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="trafficStatsListenState === 'nonCanonicalLoopback'"
|
||||
class="value-warning"
|
||||
>{{ $t("hysteria.trafficStatsNonCanonical") }}</span
|
||||
>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="trafficStats.secret">
|
||||
@@ -424,19 +439,57 @@ function list(values: string[]): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Traffic Stats API обязан слушать loopback.
|
||||
* Состояние адреса Traffic Stats API.
|
||||
*
|
||||
* Админка обращается к нему только по 127.0.0.1, поэтому любой другой адрес
|
||||
* означает, что Hysteria и панель говорят мимо друг друга: лимит устройств,
|
||||
* учёт трафика и принудительное отключение перестают работать разом.
|
||||
* Вопросов здесь ДВА, и смешивать их нельзя.
|
||||
*
|
||||
* Первый — технический: достанет ли админка до этого адреса. Она обращается к
|
||||
* API только по loopback, поэтому LAN- или публичный адрес означает, что
|
||||
* Hysteria и панель говорят мимо друг друга, и лимит устройств, учёт трафика и
|
||||
* принудительное отключение перестают работать разом.
|
||||
*
|
||||
* Второй — профильный: тот ли это адрес, который создаёт оркестратор. У
|
||||
* production-профиля он ровно один — `127.0.0.1`.
|
||||
*
|
||||
* Прежняя версия отвечала на первый вопрос и молчала по второму: `0.0.0.0`
|
||||
* считался нормой, потому что через него админка достучится. Но wildcard — не
|
||||
* loopback: внутренний control plane при нём опубликован на всех интерфейсах, и
|
||||
* при `HY2XS_FIREWALL_MODE=external|off` его не прикрывает ничто. Оркестратор
|
||||
* такой конфиг не создаёт, значит он появился правкой руками — и диагностический
|
||||
* экран обязан это назвать.
|
||||
*
|
||||
* Пустой хост (`:36712`) относится сюда же: в Go это тот же wildcard, а не
|
||||
* loopback.
|
||||
*/
|
||||
const trafficStatsIsLoopback = computed(() => {
|
||||
type TrafficStatsListenState =
|
||||
| "canonical"
|
||||
| "wildcard"
|
||||
| "nonCanonicalLoopback"
|
||||
| "unreachable";
|
||||
|
||||
const LOOPBACK_IPV4 = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
||||
|
||||
const trafficStatsListenState = computed<TrafficStatsListenState>(() => {
|
||||
const listen = profile.value.trafficStats?.listen;
|
||||
if (!listen) {
|
||||
return true;
|
||||
// Секции нет вовсе — об этом сказано отдельной плашкой выше, и второе
|
||||
// сообщение о том же было бы шумом.
|
||||
return "canonical";
|
||||
}
|
||||
const host = listen.slice(0, listen.lastIndexOf(":"));
|
||||
return host === "" || host === "127.0.0.1" || host === "0.0.0.0";
|
||||
|
||||
const separator = listen.lastIndexOf(":");
|
||||
const host = separator < 0 ? listen : listen.slice(0, separator);
|
||||
|
||||
if (host === "127.0.0.1") {
|
||||
return "canonical";
|
||||
}
|
||||
if (host === "" || host === "0.0.0.0") {
|
||||
return "wildcard";
|
||||
}
|
||||
if (LOOPBACK_IPV4.test(host)) {
|
||||
return "nonCanonicalLoopback";
|
||||
}
|
||||
return "unreachable";
|
||||
});
|
||||
|
||||
const handleExport = async () => {
|
||||
@@ -523,4 +576,10 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.value-warning {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
</style>
|
||||
|
||||
+13
-4
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user