086b5d6624
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.
293 lines
19 KiB
Markdown
293 lines
19 KiB
Markdown
# 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://` ссылка содержит фактический тип обфускации, и совместимый клиент подключается по ней напрямую
|