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:
@@ -343,6 +343,17 @@ Hysteria-интеграции с официальной документацие
|
|||||||
пережило бы `defer someOtherCleanup()` рядом со взятым замком. Теперь имя
|
пережило бы `defer someOtherCleanup()` рядом со взятым замком. Теперь имя
|
||||||
переменной берётся из самого присваивания.
|
переменной берётся из самого присваивания.
|
||||||
|
|
||||||
|
- **Панель считала wildcard нормальным адресом control plane.** Признак на
|
||||||
|
странице конфигурации отвечал только на вопрос «достучится ли админка»,
|
||||||
|
поэтому `0.0.0.0` показывался без замечаний — хотя внутренний Traffic Stats
|
||||||
|
API при нём опубликован на всех интерфейсах, а оркестратор такой конфигурации
|
||||||
|
не создаёт. Состояний теперь четыре: канон профиля, wildcard, не-канонический
|
||||||
|
loopback и недостижимый адрес; backend при этом по-прежнему отвечает только на
|
||||||
|
вопрос достижимости — превращать лишнюю публикацию в отказ обслуживания
|
||||||
|
значило бы отключить всех пиров. Заодно исправлено ложное утверждение в
|
||||||
|
комментарии `parseTrafficStatsPort`: пустой хост `:36712` — это все
|
||||||
|
интерфейсы, а не loopback.
|
||||||
|
|
||||||
### Исправлено — гейты сборки
|
### Исправлено — гейты сборки
|
||||||
|
|
||||||
- **Гейт fail-open срабатывал на корректном коде.** Проверка «авторизация не
|
- **Гейт fail-open срабатывал на корректном коде.** Проверка «авторизация не
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ export default {
|
|||||||
save: "Save",
|
save: "Save",
|
||||||
update: "Update",
|
update: "Update",
|
||||||
downloadSuccess: "Download successful",
|
downloadSuccess: "Download successful",
|
||||||
wait: "The version is being changed, please wait a moment",
|
// `wait`, `enableSuccess` and `disableSuccess` are gone together with the
|
||||||
enableSuccess: "Hysteria2 start successful",
|
// operations the panel does not have: starting, stopping and changing the
|
||||||
disableSuccess: "Hysteria2 stop successful",
|
// version of Hysteria. That lifecycle belongs to systemd and the orchestrator.
|
||||||
success: "Success",
|
success: "Success",
|
||||||
refresh: "Refresh",
|
refresh: "Refresh",
|
||||||
yes: "Yes",
|
yes: "Yes",
|
||||||
@@ -335,7 +335,11 @@ export default {
|
|||||||
trafficStatsMissing: "Traffic Stats API is not configured",
|
trafficStatsMissing: "Traffic Stats API is not configured",
|
||||||
trafficStatsMissingHint:
|
trafficStatsMissingHint:
|
||||||
"Without the trafficStats section the device limit, traffic accounting and forced disconnect do not work: the panel reaches Hysteria only through this API.",
|
"Without the trafficStats section the device limit, traffic accounting and forced disconnect do not work: the panel reaches Hysteria only through this API.",
|
||||||
trafficStatsNotLoopback:
|
trafficStatsUnreachable:
|
||||||
"the address is not loopback: the panel reaches the Traffic Stats API over 127.0.0.1 only",
|
"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: "Сохранить",
|
save: "Сохранить",
|
||||||
update: "Обновить",
|
update: "Обновить",
|
||||||
downloadSuccess: "Загрузка завершена",
|
downloadSuccess: "Загрузка завершена",
|
||||||
wait: "Версия меняется, подождите",
|
// Фразы `wait`, `enableSuccess` и `disableSuccess` удалены вместе с
|
||||||
enableSuccess: "Hysteria2 запущена",
|
// операциями, которых у панели нет: запуском, остановкой и сменой версии
|
||||||
disableSuccess: "Hysteria2 остановлена",
|
// Hysteria. Жизненный цикл принадлежит systemd и оркестратору.
|
||||||
success: "Готово",
|
success: "Готово",
|
||||||
refresh: "Обновить",
|
refresh: "Обновить",
|
||||||
yes: "Да",
|
yes: "Да",
|
||||||
@@ -346,7 +346,14 @@ export default {
|
|||||||
trafficStatsMissing: "Traffic Stats API не настроен",
|
trafficStatsMissing: "Traffic Stats API не настроен",
|
||||||
trafficStatsMissingHint:
|
trafficStatsMissingHint:
|
||||||
"Без секции trafficStats не работают лимит устройств, учёт трафика и принудительное отключение: панель обращается к Hysteria только через этот API.",
|
"Без секции 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)">{{
|
<span :class="valueClass(profile.trafficStats.listen)">{{
|
||||||
text(profile.trafficStats.listen)
|
text(profile.trafficStats.listen)
|
||||||
}}</span>
|
}}</span>
|
||||||
|
<!--
|
||||||
|
Недостижимость и «шире, чем нужно» — разные новости: первая
|
||||||
|
означает, что доступ пиров уже не работает, вторая — что
|
||||||
|
внутренний API опубликован лишний раз.
|
||||||
|
-->
|
||||||
<span
|
<span
|
||||||
v-if="!trafficStatsIsLoopback"
|
v-if="trafficStatsListenState === 'unreachable'"
|
||||||
class="value-error"
|
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>
|
||||||
<el-descriptions-item label="trafficStats.secret">
|
<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;
|
const listen = profile.value.trafficStats?.listen;
|
||||||
if (!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 () => {
|
const handleExport = async () => {
|
||||||
@@ -523,4 +576,10 @@ onMounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--el-color-danger);
|
color: var(--el-color-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value-warning {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
}
|
||||||
</style>
|
</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.1:36712" -> канонический production-профиль;
|
||||||
// "127.0.0.5:36712" -> любой loopback достижим по loopback;
|
// "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 принимается именно потому, что связь между компонентами при нём
|
// Wildcard принимается именно потому, что связь между компонентами при нём
|
||||||
// РАБОТАЕТ: отказывать в обслуживании из-за лишней публикации внутреннего API
|
// РАБОТАЕТ: отказывать в обслуживании из-за лишней публикации внутреннего API
|
||||||
// значило бы наказывать за неё отключением всех пиров. Запрет на wildcard —
|
// значило бы наказывать за неё отключением всех пиров. Сама лишняя публикация
|
||||||
// решение оркестратора, который конфиг создаёт (см. validateRuntimeConfig).
|
// при этом не остаётся незамеченной — о ней сообщает панель.
|
||||||
func parseTrafficStatsPort(listen string) (int64, error) {
|
func parseTrafficStatsPort(listen string) (int64, error) {
|
||||||
trimmed := strings.TrimSpace(listen)
|
trimmed := strings.TrimSpace(listen)
|
||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
return 0, errors.New("empty listen")
|
return 0, errors.New("empty listen")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Пустой хост подставляется только затем, чтобы SplitHostPort принял
|
||||||
|
// строку: сам по себе `:36712` означает все интерфейсы, и loopback среди
|
||||||
|
// них, поэтому подстановка не меняет ответа на вопрос о достижимости.
|
||||||
hostPort := trimmed
|
hostPort := trimmed
|
||||||
if strings.HasPrefix(trimmed, ":") {
|
if strings.HasPrefix(trimmed, ":") {
|
||||||
hostPort = "127.0.0.1" + trimmed
|
hostPort = "127.0.0.1" + trimmed
|
||||||
|
|||||||
@@ -24,14 +24,16 @@ func TestParseTrafficStatsPortAcceptsReachableAddresses(t *testing.T) {
|
|||||||
want int64
|
want int64
|
||||||
}{
|
}{
|
||||||
{"канонический профиль", "127.0.0.1:36712", 36712},
|
{"канонический профиль", "127.0.0.1:36712", 36712},
|
||||||
{"хост не задан", ":36712", 36712},
|
|
||||||
{"пробелы по краям", " 127.0.0.1:36712 ", 36712},
|
{"пробелы по краям", " 127.0.0.1:36712 ", 36712},
|
||||||
{"другой адрес loopback", "127.0.0.5:36712", 36712},
|
{"другой адрес loopback", "127.0.0.5:36712", 36712},
|
||||||
// Wildcard включает loopback: обмен состоится, и отказывать в
|
// Wildcard включает loopback: обмен состоится, и отказывать в
|
||||||
// обслуживании из-за лишней публикации внутреннего API нельзя —
|
// обслуживании из-за лишней публикации внутреннего API нельзя —
|
||||||
// это отключило бы всех пиров. Запрет на wildcard принадлежит
|
// это отключило бы всех пиров. Запрет на wildcard принадлежит
|
||||||
// оркестратору, который конфиг и создаёт.
|
// оркестратору, который конфиг и создаёт, а сообщает о нём панель.
|
||||||
{"wildcard", "0.0.0.0:36712", 36712},
|
{"wildcard", "0.0.0.0:36712", 36712},
|
||||||
|
// Пустой хост — тот же wildcard, а не loopback: в Go `:36712`
|
||||||
|
// означает все интерфейсы.
|
||||||
|
{"хост не задан", ":36712", 36712},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ rc3](2026-09-02-v1.0.0-rc3-preflight-findings.md) и сверка Hysteria-ин
|
|||||||
| ADM-HY2-08 | Мёртвые остатки прежней архитектуры | P4 | закрыт |
|
| ADM-HY2-08 | Мёртвые остатки прежней архитектуры | P4 | закрыт |
|
||||||
| GATE-03 | Гейт освобождения admission-замка проверял форму, а не замок | P3 | закрыт |
|
| GATE-03 | Гейт освобождения admission-замка проверял форму, а не замок | P3 | закрыт |
|
||||||
| DOC-03 | Отчёт rc3 не называл, к какому дереву относятся выводы | P3 | закрыт |
|
| DOC-03 | Отчёт rc3 не называл, к какому дереву относятся выводы | P3 | закрыт |
|
||||||
|
| DIAG-01 | Панель считала wildcard нормальным состоянием control plane | P3 | закрыт |
|
||||||
|
|
||||||
ROT-01 и ADM-HY2-01…08 пришли внешним разбором; уточнения ниже — при проверке
|
ROT-01 и ADM-HY2-01…08 пришли внешним разбором; уточнения ниже — при проверке
|
||||||
его выводов по коду и исходникам upstream. Три из них меняли предложенное
|
его выводов по коду и исходникам upstream. Три из них меняли предложенное
|
||||||
@@ -149,6 +150,43 @@ LAN, публичный адрес), `apps/service/config_traffic_stats_test.go`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## DIAG-01 — панель считала wildcard нормальным состоянием control plane
|
||||||
|
|
||||||
|
Найдено при перепроверке фикса ADM-HY2-01 и относится только к диагностике: на
|
||||||
|
data plane не влияет.
|
||||||
|
|
||||||
|
Первая версия признака на странице конфигурации отвечала на вопрос
|
||||||
|
«достучится ли админка» и молчала по второму, не менее важному, — «тот ли это
|
||||||
|
адрес, который создаёт оркестратор»:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
host === "" || host === "127.0.0.1" || host === "0.0.0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
`0.0.0.0` — не loopback, а wildcard: внутренний control plane при нём
|
||||||
|
опубликован на всех интерфейсах, и при `HY2XS_FIREWALL_MODE=external|off` его не
|
||||||
|
прикрывает ничто. Оркестратор такой конфигурации не создаёт, значит она
|
||||||
|
появилась правкой руками — и диагностический экран обязан это назвать, а не
|
||||||
|
показывать как норму. Официальная документация Traffic Stats API отдельно
|
||||||
|
предупреждает об ограничении доступа к этому listener'у.
|
||||||
|
|
||||||
|
Там же обнаружилась вторая неточность, уже в моей формулировке: **пустой хост
|
||||||
|
(`:36712`) — тоже wildcard, а не loopback.** В Go `:port` означает все
|
||||||
|
интерфейсы; комментарий в `parseTrafficStatsPort` утверждал обратное. На
|
||||||
|
поведение это не влияло (wildcard включает loopback, поэтому обмен состоится),
|
||||||
|
но описание контракта было ложным и исправлено.
|
||||||
|
|
||||||
|
**Как закрыто.** Backend не тронут: он по-прежнему отвечает ровно на вопрос
|
||||||
|
достижимости, и превращать лишнюю публикацию в отказ обслуживания нельзя — это
|
||||||
|
отключило бы всех пиров. Разделены понятия на стороне панели:
|
||||||
|
|
||||||
|
| адрес | состояние | что показано |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `127.0.0.1` | `canonical` | без пометки |
|
||||||
|
| `0.0.0.0`, пустой хост | `wildcard` | предупреждение: API доступен, но слушает все интерфейсы |
|
||||||
|
| `127.0.0.x` | `nonCanonicalLoopback` | предупреждение: доступен, но оркестратор такого не создаёт |
|
||||||
|
| прочее | `unreachable` | ошибка: доступ пиров уже не работает |
|
||||||
|
|
||||||
## ADM-HY2-02 — JSON-журнал не разбирался ни одной строкой
|
## ADM-HY2-02 — JSON-журнал не разбирался ни одной строкой
|
||||||
|
|
||||||
Юнит запускает Hysteria с `HYSTERIA_LOG_FORMAT=json`. Разбор складывал запись
|
Юнит запускает Hysteria с `HYSTERIA_LOG_FORMAT=json`. Разбор складывал запись
|
||||||
@@ -370,4 +408,7 @@ go test -race ./service/... -count=1
|
|||||||
6. открыть страницу журнала Hysteria: записи разобраны, контекст виден,
|
6. открыть страницу журнала Hysteria: записи разобраны, контекст виден,
|
||||||
секретов нет;
|
секретов нет;
|
||||||
7. добавить в конфиг секцию вне профиля (например `resolver`) и открыть страницу
|
7. добавить в конфиг секцию вне профиля (например `resolver`) и открыть страницу
|
||||||
конфигурации: секция обязана попасть в «расхождение конфигурации».
|
конфигурации: секция обязана попасть в «расхождение конфигурации»;
|
||||||
|
8. заменить `trafficStats.listen` на `0.0.0.0:36712`: продукт продолжает
|
||||||
|
работать, а страница конфигурации показывает предупреждение о публикации на
|
||||||
|
всех интерфейсах.
|
||||||
|
|||||||
@@ -378,7 +378,18 @@ trafficStats.listen слушает 192.168.1.10, а админка обраща
|
|||||||
API только по loopback. ...Верните 127.0.0.1 через `hy2xs-orchestrator reconfigure`
|
API только по loopback. ...Верните 127.0.0.1 через `hy2xs-orchestrator reconfigure`
|
||||||
```
|
```
|
||||||
|
|
||||||
Страница конфигурации показывает тот же адрес и помечает его как не-loopback.
|
Страница конфигурации показывает тот же адрес и называет его состояние. Ответов
|
||||||
|
три, и они означают разное:
|
||||||
|
|
||||||
|
| адрес | что показано | что это значит |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `127.0.0.1:36712` | без пометки | канон production-профиля |
|
||||||
|
| `0.0.0.0:36712`, `:36712` | предупреждение | API достижим, но опубликован на всех интерфейсах; при `HY2XS_FIREWALL_MODE=external\|off` его не прикрывает ничто |
|
||||||
|
| `127.0.0.5:36712` | предупреждение | достижим, но оркестратор такого не создаёт — конфиг правили руками |
|
||||||
|
| `192.168.1.10:36712` | ошибка | панель до него не достучится, доступ пиров уже не работает |
|
||||||
|
|
||||||
|
Пустой хост в `listen` — это не loopback: в Go `:36712` означает все интерфейсы,
|
||||||
|
ровно как `0.0.0.0`.
|
||||||
|
|
||||||
### Дашборд показывает «состояние службы неизвестно»
|
### Дашборд показывает «состояние службы неизвестно»
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
27. живая сессия, которой в базе больше ничего не соответствует (пир удалён либо его `auth_id` заменён импортом, а разрыв в тот момент не удался), завершается очередным циклом учёта — не позднее 30 секунд
|
27. живая сессия, которой в базе больше ничего не соответствует (пир удалён либо его `auth_id` заменён импортом, а разрыв в тот момент не удался), завершается очередным циклом учёта — не позднее 30 секунд
|
||||||
28. превышение `maxDevices` живыми сессиями устраняется тем же циклом: после неудавшегося разрыва при снижении лимита повтор формы даёт успех без `/kick`, и единственный механизм схождения здесь — cron
|
28. превышение `maxDevices` живыми сессиями устраняется тем же циклом: после неудавшегося разрыва при снижении лимита повтор формы даёт успех без `/kick`, и единственный механизм схождения здесь — cron
|
||||||
29. смена секрета пира меняет его `auth_id`: клиент со старым секретом теряет доступ не позднее 30 секунд даже в том случае, когда `/kick` прошёл успешно, а соединение зарегистрировалось после него
|
29. смена секрета пира меняет его `auth_id`: клиент со старым секретом теряет доступ не позднее 30 секунд даже в том случае, когда `/kick` прошёл успешно, а соединение зарегистрировалось после него
|
||||||
30. `trafficStats.listen` слушает `127.0.0.1`; конфиг с не-loopback адресом админка отвергает с явным сообщением, а не молча ходит на loopback
|
30. `trafficStats.listen` слушает `127.0.0.1`; конфиг с не-loopback адресом админка отвергает с явным сообщением, а не молча ходит на loopback. Страница конфигурации различает три состояния адреса: канон профиля, достижимый но опубликованный шире необходимого (`0.0.0.0`, пустой хост, прочий loopback) и недостижимый
|
||||||
31. `hysteria-server.service` запущен с `HYSTERIA_DISABLE_UPDATE_CHECK=1`: внешних запросов проверки версии при старте нет
|
31. `hysteria-server.service` запущен с `HYSTERIA_DISABLE_UPDATE_CHECK=1`: внешних запросов проверки версии при старте нет
|
||||||
32. дашборд различает «служба остановлена» и «состояние службы неизвестно»; доступность Traffic Stats API показывается независимо от ответа systemd
|
32. дашборд различает «служба остановлена» и «состояние службы неизвестно»; доступность Traffic Stats API показывается независимо от ответа systemd
|
||||||
33. страница журнала Hysteria показывает разобранные `level`/`time`/`msg` и структурный контекст, а не сырой JSON
|
33. страница журнала Hysteria показывает разобранные `level`/`time`/`msg` и структурный контекст, а не сырой JSON
|
||||||
|
|||||||
@@ -543,7 +543,37 @@ describe("страница конфигурации Hysteria", () => {
|
|||||||
const source = configPage();
|
const source = configPage();
|
||||||
|
|
||||||
expect(source).toContain("hysteria.trafficStatsMissing");
|
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 экран не имеет права быть щедрее санитизированной выгрузки того
|
// Read-only экран не имеет права быть щедрее санитизированной выгрузки того
|
||||||
|
|||||||
Reference in New Issue
Block a user