Files
HY2XS_flamy/docs/04-admin-panel.md
T
founder 086b5d6624 build: закрепить новые инварианты приёмкой и документацией
verify_versions_contract получил сверку API namespace. Путь machine-auth
записывается в /etc/hysteria/config.yaml и в post-install.env, то есть по нему
Hysteria обращается к админке. Пока строка была продублирована в шаблонах,
smoke, тестах, приёмке и e2e, расхождение обнаруживалось только на живом
сервере. Теперь Go-константы, API_BASE фронтенда и оба шаблона сверяются
против значений, скомпилированных в оркестратор.

Приёмка проверяет, что:
  - fatal_pre_apply недостижим после записи install-state;
  - каждый ownership-флаг взводится раньше своего шага;
  - у read-only фазы нет универсального раннера, через который можно
    проскользнуть;
  - инвариант публичного endpoint живёт в preflight и не обращается к внешним
    сервисам определения IP;
  - purge-v0.sh и clean-host описывают одну границу;
  - секреты не попадают в персистентный файл экспорта;
  - импорт пиров валидируется так же строго, как их создание;
  - удалённые exportConfig/importConfig не вернулись.

Захардкоженная схема =2 в приёмке заменена на значение из versions.env: при
переходе на schema 3 пришлось бы помнить ещё и про эту строку.

Документация: контракт раннеров и ownership в 08, инвариант публичного
endpoint в 08/09/12/13 и README, сетевая идентичность панели и удалённые
export/import в 04, сценарии D1 (отказ сразу после PHASE 0) и D2 (устаревший
DNS после смены IPv4) в 11, версии package.json как не-версия продукта в 02.
2026-08-27 20:50:03 +05:00

293 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Admin panel: HY2XS admin
## Цель документа
Зафиксировать модель работы с admin-панелью: HY2XS admin является **штатным компонентом HY2XS**, а не внешней зависимостью, которую target server где-то добывает во время установки.
## Место компонента в системе
Проект состоит из компонентов двух разных типов:
- **Hysteria2** — external runtime dependency. Ванильный upstream-бинарник, который оркестратор забирает из официального источника во время установки.
- **HY2XS admin** — native HY2XS component. Исходный код лежит в репозитории, компонент собирается production builder'ом вместе с остальными артефактами HY2XS.
Это противопоставление и есть основная архитектурная граница.
## Состав компонента
- исходный код admin-компонента хранится в [`apps/`](../apps);
- backend реализован на Go;
- frontend реализован на Vue/Vite;
- frontend-ассеты встраиваются в Go-бинарник;
- компонент собирается production builder'ом вместе с остальными артефактами HY2XS;
- готовый бинарник едет в install package как `ui/hy2xs-admin/hy2xs-admin`.
## Правила поставки
HY2XS admin:
- поставляется внутри итогового пакета;
- имеет свой install dir;
- имеет свой data dir;
- запускается отдельным rootless systemd unit (`hy2xs-admin`);
- не требует target-side build.
Target server **не собирает** admin-компонент из исходного кода и **не скачивает** его из внешнего репозитория.
## Scope панели
Панель нужна для:
- operator-facing управления;
- просмотра статуса;
- работы с пользователями / трафиком / сущностями доступа;
- удобной админской рутины.
Панель не должна:
- определять install lifecycle сервера;
- превращать систему в сложный control plane;
- диктовать scope оркестратора.
HY2XS admin работает как надстройка над Hysteria YAML/API-слоем. Это нормально: важно только, чтобы источник истины по runtime-состоянию был понятен и не было двух конкурирующих конфигурационных миров без правил синхронизации.
Относительно конфигурации Hysteria панель **read-only**: конфиг генерирует оркестратор.
## Сетевая идентичность панели принадлежит оркестратору
Панель не конфигурирует себя сама.
| Величина | Источник |
| --- | --- |
| Порт панели | `HY2XS_UI_PORT``ExecStart … -p <port>` |
| Адрес привязки | `HY2XS_UI_BIND_HOST` из `/etc/hy2xs/hy2xs.env` |
| Каталог данных | `HY2XS_DATA_DIR` |
| Каталог логов | `HY2XS_LOG_DIR` |
| Маршрут панели | всегда `/` |
| TLS | терминируется снаружи (SSH-туннель или reverse proxy) |
До v1 эти величины дублировались в таблице `config` под ключами
`H_UI_WEB_PORT`, `H_UI_WEB_CONTEXT`, `H_UI_CRT_PATH`, `H_UI_KEY_PATH`
наследие H UI, где панель публиковалась наружу самостоятельно. Получался круг:
оркестратор передавал порт аргументом, панель записывала его в SQLite и тут же
читала обратно, а UI показывал поля в disabled-виде. Ни одного факта база при
этом не добавляла.
В v1 этих ключей нет ни в схеме, ни в seed, ни в интерфейсе. Собственного
TLS-слоя у панели тоже нет: production-контракт — `HY2XS_UI_BIND_HOST=127.0.0.1`
и `HY2XS_UI_PUBLIC_ACCESS=false`, то есть внутренний сервис. Если панели
когда-нибудь понадобится публичный endpoint, TLS обязан заканчиваться на
ingress/reverse-proxy, а не возвращаться к модели H UI.
## Пространства имён HTTP API
| Префикс | Назначение | Middleware |
| --- | --- | --- |
| `/healthz` | liveness/readiness | нет |
| `/internal/hysteria/auth` | machine-to-machine: Hysteria спрашивает разрешение на подключение пира | `LocalOnly` + `MachineAuth` |
| `/api/...` | операторский и auth API | rate limiter, JWT, admin |
Разделение отражает разницу в природе маршрутов. `/internal/hysteria/auth`
не интерфейс для человека и не часть операторского API: это внутренний
IPC-подобный HTTP endpoint между двумя процессами на одной машине. До v1 он
лежал под тем же префиксом `hui`, что и JWT-защищённый админский API, хотя
middleware у них не пересекаются.
Путь machine-auth — **runtime-контракт продукта**: он записывается в
`/etc/hysteria/config.yaml` и в `post-install.env`. Поэтому он объявлен ровно
в двух местах — `constant.HysteriaMachineAuthPath` в админке и
`HYSTERIA_MACHINE_AUTH_PATH` в оркестраторе, — а сборка сверяет их между собой
и с шаблонами.
## Импорт и экспорт
| Операция | Статус |
| --- | --- |
| Экспорт пиров (`POST /api/peer-export`) | есть |
| Импорт пиров (`POST /api/peer-import`) | есть |
| Экспорт конфига Hysteria (`POST /api/config/exportHysteria2Config`) | есть, с вырезанием секретов |
| Экспорт/импорт таблицы `config` | **удалён** |
Generic-выгрузка таблицы `config` отдавала её целиком, исключая только сырой
Hysteria YAML. В той же таблице лежат `JWT_SECRET`, `PEER_SECRET_KEY`,
`PEER_SECRET_ENCRYPTION_KEY` и `HYSTERIA2_TRAFFIC_STATS_SECRET`: кнопка
«Export» выгружала их в открытом виде, а зеркальный импорт позволял их
подменить. Для `PEER_SECRET_ENCRYPTION_KEY` это не только вопрос секретности —
после подмены перестают расшифровываться секреты уже существующих пиров.
Осмысленного production-сценария у этой пары не было: конфигурацией сервера
владеет оркестратор, перенос пиров делают `peer-import`/`peer-export`.
Оба оставшихся экспорта формируются **в памяти** и отдаются прямо в ответ.
Раньше они шли через `os.Create` в `/var/lib/hy2xs-admin/export/`, и файл там
оставался навсегда — при `?includeSecrets=true` это означало расшифрованные
секреты пиров на диске, накапливающиеся с каждым нажатием кнопки. Каталога
`export/` больше не существует.
Импорт пиров проверяется так же строго, как обычное создание пира: те же
правила для имени, quota, `maxDevices`, `disabled`, длины секрета. Дополнительно:
- неизвестные поля в JSON отклоняются, а не игнорируются молча;
- партия проверяется целиком **до** первой записи в базу — файл применяется
полностью или не применяется вовсе;
- пир `bootstrap-admin-peer` защищён от перезаписи: его секрет продублирован
в `/etc/hy2xs/bootstrap-admin.secret`.
## Два слоя работы с конфигом Hysteria
Это важное архитектурное разделение.
| Слой | Назначение | Поведение при неизвестных полях |
| --- | --- | --- |
| Типизированная модель | отображение известных HY2XS полей в UI | неизвестные поля не отображаются |
| Сырой YAML | экспорт и сохранение | неизвестные поля **сохраняются** |
Причина: если бы экспорт работал через типизированную модель (`Unmarshal` → структура → `Marshal`), то любое поле, о котором HY2XS ещё не знает, терялось бы при round-trip. Панель незаметно урезала бы современный конфиг.
Поэтому:
- экспорт читает исходный YAML и сохраняет структуру документа целиком;
- будущие версии Hysteria не ломают экспорт только потому, что backend и frontend ещё не научились показывать новый параметр;
- это прямое следствие модели «latest stable на сборке»: схема upstream может опережать модель HY2XS.
### Санитайз экспорта
Экспортируемый файл покидает сервер, поэтому секреты из него вырезаются:
- пароли обфускации (`obfs.*.password`);
- `trafficStats.secret`;
- `access_token` в auth-URL и учётные данные, встроенные в URL;
- `auth.password`, `auth.userpass`;
- учётные данные ACME DNS-провайдера;
- **неизвестные** поля с секретоподобным именем: `password`, `passwd`,
`passphrase`, `secret`, `token`, `credential`, `apiKey` / `api_key`,
`privateKey` / `private_key`, `accessKey`, `secretKey`, `authorization`,
`cookie`, `bearer`, `signature`.
Последний пункт — обратная сторона сохранения неизвестных полей: новое
upstream-поле с секретом вырезается ещё до того, как HY2XS про него узнает.
### Как формулируется гарантия
Точная формулировка:
> вырезаются известные секреты и неизвестные поля с секретоподобным именем.
Не «любой будущий секрет будет автоматически удалён». Обобщённый sanitizer
работает по именам полей и не может предугадать произвольное имя, которое
upstream выберет для нового секрета. Список маркеров синхронизирован с
`orchestrator/src/lib/redaction.ts`; при появлении нового поля его нужно
добавить в оба места.
Пути к файлам (`tls.cert`, `tls.key`, `ech.keyPath`, `tls.clientCA`) секретами
не считаются и остаются читаемыми — они нужны для диагностики.
## Модель современной схемы Hysteria
Модель админки понимает актуальную серверную схему, даже там, где UI не позволяет ничего включить: `obfs.gecko`, `ech`, `congestion`, `mimic`, `realm`, `tls.clientCA`, `quic.disableStatelessReset`, `bandwidth.disableLossCompensation`, `masquerade.proxy.xForwarded`.
Смысл в том, чтобы admin **понимал текущую upstream-схему**, а не считал неизвестными поля собственного конфига.
## Генерация клиентских ссылок
- тип обфускации и пароль берутся из фактического конфига одинаково для всех поддерживаемых типов (`gecko`, `salamander`);
- неизвестный тип обфускации в ссылку не попадает: лучше отсутствие параметра, чем параметр, который клиент не понимает;
- публичный endpoint берётся из `HY2XS_PUBLIC_HOST` + `HY2XS_PUBLIC_PORT`, а не из `listen` или Host-заголовка запроса;
- SNI берётся из ACME-домена, затем из `HY2XS_DOMAIN`, затем из `HY2XS_PUBLIC_HOST`; IP-адрес как SNI не используется;
- `minPacketSize`/`maxPacketSize` Gecko в ссылку не помещаются — поэтому HY2XS держит их на upstream-defaults `512/1200`.
## Правила ответственности
### Source of truth
- runtime transport layer: Hysteria2
- операторский UI layer: HY2XS admin
- install lifecycle: оркестратор HY2XS
- deploy facts: `post-install.env`
## Production lifecycle Hysteria2
В production package HY2XS admin не скачивает и не обновляет бинарь Hysteria2 самостоятельно.
Правильная модель:
- Hysteria2 устанавливается install-оркестратором с official upstream;
- Hysteria2 запускается отдельным `hysteria-server.service`;
- HY2XS admin работает как operator UI и HTTP auth/traffic layer;
- HY2XS admin не запускается от root;
- смена версии Hysteria2 через UI **отсутствует как API**;
- список upstream releases не является частью operator UI baseline;
- port hopping не является частью production path.
### Удалённые операции: почему не заглушки
Маршруты, которые продукт принципиально не поддерживает, **удалены**, а не
оставлены отвечающими «feature disabled»:
| Удалённый маршрут | Кто владеет операцией |
| --- | --- |
| `POST /hysteria2ChangeVersion` | install-оркестратор |
| `GET /listRelease` | build layer |
| `POST /config/updateHysteria2Config` | install-оркестратор |
| `POST /config/importHysteria2Config` | install-оркестратор |
| `POST /config/restartServer` | systemd |
| `POST /config/uploadCertFile` | оператор + оркестратор |
| `GET /config/hysteria2AcmePath` | не имел потребителя |
| `POST /config/exportConfig` | выгружал JWT- и peer-ключи в открытом виде |
| `POST /config/importConfig` | позволял подменить те же ключи |
Причины две.
Во-первых, API-контракт не должен даже обещать updater, которого у продукта
нет: маршрут, всегда возвращающий отказ, вводит в заблуждение.
Во-вторых, это лишняя attack surface и технический мусор от прежней
архитектуры.
Вместе с маршрутами удалены соответствующие клиентские функции фронтенда,
кнопки и строки i18n. Кнопка, которая гарантированно возвращает ошибку, —
не «точка расширения на будущее», а дефект UX. Возвращение любого из этих
маршрутов ломает acceptance-проверку сборки.
Конфигурация Hysteria остаётся доступной панели **на чтение и на выгрузку**:
`GET /config/getHysteria2Config` и `POST /config/exportHysteria2Config`.
### Что нельзя делать
- собирать admin-компонент на target server;
- скачивать admin-компонент на target из внешнего репозитория;
- склеивать unit Hysteria2 и unit HY2XS admin в один сервис;
- раздувать оркестратор из-за особенностей панели;
- использовать HY2XS admin как updater бинаря Hysteria2;
- использовать `JWT_SECRET` как `trafficStats.secret` для Hysteria API;
- экспортировать конфиг Hysteria через типизированную модель — так теряются неизвестные upstream-поля;
- выгружать конфиг с секретами в открытом виде.
## Что фиксировать в `post-install.env`
Минимум:
- `HY2XS_ADMIN_ENABLED`
- `HY2XS_ADMIN_SOURCE`
- `HY2XS_ADMIN_BUILD_ID`
- `HY2XS_ADMIN_BIND_HOST`
- `HY2XS_ADMIN_PORT`
- `HY2XS_ADMIN_INSTALL_DIR`
- `HY2XS_ADMIN_DATA_DIR`
- `HY2XS_ADMIN_LOG_DIR`
## Инварианты
Схема считается корректной, если:
1. HY2XS admin приезжает на target уже в составе пакета
2. target не скачивает и не собирает admin-компонент
3. HY2XS admin работает отдельным сервисом
4. HY2XS admin не меняет install-only scope оркестратора
5. Hysteria2 остаётся внешним vanilla upstream-компонентом
6. HY2XS admin не выступает updater-менеджером Hysteria2
7. `trafficStats.secret` не связан с `JWT_SECRET`
8. экспорт конфига сохраняет неизвестные upstream-поля
9. экспорт конфига не содержит секретов
10. сгенерированная `hysteria2://` ссылка содержит фактический тип обфускации, и совместимый клиент подключается по ней напрямую