# 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 ` | | Адрес привязки | `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://` ссылка содержит фактический тип обфускации, и совместимый клиент подключается по ней напрямую