672d455467
Hardening-проход перед первой сборкой на Debian. Три из найденного не воспроизводились ни на одном dry-run и проявились бы только на живом сервере. Установка * preflight внутри install вызывался дважды и оба раза проверял clean-host. Ко второму вызову на диске лежал собственный /var/lib/hy2xs/install-state.json, записанный после первого preflight, и опознавался как маркер посторонней установки: КАЖДАЯ чистая установка падала сразу после apt-get с fatal_post_apply и оставляла сервер наполовину настроенным. Чистота хоста — условие входа в операцию, возможности платформы проверяются уже внутри PHASE 1, поэтому checkCleanHost стал отдельным параметром без умолчания. * PHASE 1 начиналась в install.sh: shell сам создавал /usr/local/lib/hy2xs, ставил бинарник, вешал symlink и копировал runtime-пакет, и только потом запускал оркестратор с его собственным preflight. Отказ того preflight объявлялся fatal_pre_apply — «на сервере ничего не изменено» — при уже созданном каталоге оркестратора. Отследить владение мутацией невозможно, пока мутируют двое: install.sh больше не изменяет ничего, раскладку выполняет steps/bootstrap.ts под ownership.bootstrapTouched, пути попали в owned_paths. Как следствие удалено деление clean-host на фазы. * diagnosticsCollect стояла перед rollback обычным await в install и в reconfigure. На заполненном диске она падает сама и отменяла откат целиком. Диагностика — best effort, откат — обязателен. * reconfigure/repair выбирали записываемую фазу отказа регулярным выражением по тексту ошибки. Переведено на ownership-флаги. Секреты * Журнал админки писал RequestURI, то есть путь вместе с query. Hysteria обращается к /internal/hysteria/auth?access_token=<секрет> при каждом подключении пира, поэтому действующий machine token оседал открытым текстом в hy2xs-admin.log, который отдаётся через ExportLog и попадает в diagnostics-бандл. Логируется путь; значения query не пишутся, имена — пишутся. Канала было два: gin.Default() печатает path?query в stdout, оттуда в journald и в тот же бандл, — панель переведена на gin.New() + Recovery(). Журналы внутри бандла и журнал Hysteria из ExportLog теперь проходят санитайз. Сравнение токена — constant time. * Config API позволял прочитать и подменить ключи приложения: getConfig и listConfig принимали произвольный ключ, а проверка записи была denylist'ом из трёх ключей оркестратора. Запрос ?key=PEER_SECRET_ENCRYPTION_KEY отдавал master-key шифрования секретов пиров. Доступ переведён на allowlist, маршрут getConfig удалён целиком — потребителей у него не было ни одного. Пиры * Импорт применялся по одной записи вне транзакции, вопреки собственному контракту. Валидация не знает, что уже лежит в базе: cross-conflict по UNIQUE(name) оставлял часть файла применённой. Применение выполняется одной транзакцией, криптоматериал считается до её открытия. * Файл импорта мог содержать хвостовой JSON-документ, который молча не применялся. После разбора проверяется io.EOF. * Экспорт разделён на «Экспорт настроек» и «Резервная копия» с секретами и подтверждением: обычный экспорт выдаёт пирам новые секреты при импорте, и прежние клиентские ссылки после переноса переставали работать. Сборка * Два stale-грепа в приёмке роняли build.sh в самом конце, внутри verify_archive. Первый искал в smoke.ts исчезнувший литерал URL, второй совпадал с router_test.go, который перечисляет удалённые маршруты, потому что проверяет их отсутствие: добавление регрессионного теста ломало сборку. * verify_archive требовал наличия мутирующей строки в install.sh. Инвариант перевёрнут: их не должно быть ни одной. Очистка * Удалены entity.LegacyAccount, миграции 002/003 и мёртвые хелперы listSQLMigrationFiles и envInt: v1 не мигрирует базу 0.x ни при каком сценарии. Номера оставшихся миграций сохранены. H UI-словарь убран из обычных доков, в docs/14 он остаётся — там это имена объектов для удаления. * Список непубличных IPv4 приведён к IANA Special-Purpose Address Registry: 203.0.113.5 из RFC-примеров считался публичным адресом сервера. Отказ резолвера отделён от отсутствия A-записи. Проверено: bun test 233, go test 71, tsc/vue-tsc, bash -n 11 скриптов, приёмка прогнана против дерева.
397 lines
27 KiB
Markdown
397 lines
27 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` собственными ключами
|
||
панели: оркестратор передавал порт аргументом, панель записывала его в 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, а не возвращаться к модели «панель публикует себя сама».
|
||
|
||
Имена ключей предыдущего поколения намеренно не приводятся: в обычных v1-доках
|
||
их словаря нет. Всё, что нужно для распознавания и удаления старой установки, —
|
||
в [14-legacy-cleanup.md](14-legacy-cleanup.md).
|
||
|
||
## Пространства имён 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 он
|
||
лежал под тем же префиксом, что и JWT-защищённый админский API, хотя
|
||
middleware у них не пересекаются.
|
||
|
||
Путь machine-auth — **runtime-контракт продукта**: он записывается в
|
||
`/etc/hysteria/config.yaml` и в `post-install.env`. Поэтому он объявлен ровно
|
||
в двух местах — `constant.HysteriaMachineAuthPath` в админке и
|
||
`HYSTERIA_MACHINE_AUTH_PATH` в оркестраторе, — а сборка сверяет их между собой
|
||
и с шаблонами.
|
||
|
||
## Журнал запросов не содержит значений query-параметров
|
||
|
||
Hysteria обращается к машинному endpoint'у как
|
||
`/internal/hysteria/auth?access_token=<machine token>` — при каждом подключении
|
||
пира. Поэтому в журнале админки пишется **путь**, а не `RequestURI`:
|
||
|
||
```json
|
||
{ "reqMethod": "POST", "reqPath": "/internal/hysteria/auth", "reqQueryKeys": "access_token" }
|
||
```
|
||
|
||
Пока логировался `RequestURI`, действующий machine token оседал открытым
|
||
текстом в `/var/log/hy2xs/hy2xs-admin.log`. Этот файл отдаётся оператору через
|
||
`ExportLog` и попадает в diagnostics-бандл, то есть секрет утекал наружу в
|
||
штатном режиме работы — мимо всей структурной редакции, сделанной для конфигов
|
||
и env.
|
||
|
||
Значения query-параметров не логируются вовсе: список «что можно» пришлось бы
|
||
вести вручную, и он неизбежно разошёлся бы с набором маршрутов. Имена
|
||
параметров сохранены — для диагностики их достаточно.
|
||
|
||
Каналов журналирования у панели ровно один. Админка запускается через
|
||
`gin.New()` + `gin.Recovery()`, а не `gin.Default()`: штатный `gin.Logger()`
|
||
печатает путь **вместе с query string** в stdout, откуда он уходит в journald, а
|
||
оттуда — в diagnostics-бандл. Это был второй, независимый канал той же утечки, и
|
||
починка собственного логгера его бы не закрыла.
|
||
|
||
Журнал Hysteria (`ExportLog`, вкладка логов) проходит через санитайз
|
||
`service.SanitizeLogText`: `HY2_AUTH_URL` несёт `access_token`, и upstream волен
|
||
упомянуть его в сообщении об ошибке обращения к auth-backend. Санитайз
|
||
сохраняет host, port и path — диагностика от него не страдает. Тот же проход
|
||
применяется к `journal-*.log` внутри diagnostics-бандла оркестратора.
|
||
|
||
## Импорт и экспорт
|
||
|
||
| Операция | Статус |
|
||
| --- | --- |
|
||
| Экспорт пиров (`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`.
|
||
|
||
### Точечный доступ к таблице `config` — по allowlist
|
||
|
||
Удаления generic-пары оказалось недостаточно. Опасность осталась в точечном
|
||
API: `getConfig` и `listConfig` принимали произвольный ключ, а проверка записи
|
||
работала denylist'ом из трёх ключей оркестратора. То есть авторизованный запрос
|
||
`?key=PEER_SECRET_ENCRYPTION_KEY` отдавал master-key шифрования секретов пиров,
|
||
а `updateConfigs` позволял подменить `JWT_SECRET` и оба peer-ключа. Отверстие
|
||
сменило размер, но не исчезло.
|
||
|
||
В v1:
|
||
|
||
| Ключ | Чтение | Запись |
|
||
| --- | --- | --- |
|
||
| `HYSTERIA2_TRAFFIC_TIME` | да | да |
|
||
| `RESET_TRAFFIC_CRON` | да | да |
|
||
| `HYSTERIA2_CONFIG_REMARK` | да | нет |
|
||
| `HYSTERIA2_ENABLE`, `HYSTERIA2_CONFIG`, `HYSTERIA2_TRAFFIC_STATS_SECRET` | нет | нет, владелец — оркестратор |
|
||
| `JWT_SECRET`, `PEER_SECRET_KEY`, `PEER_SECRET_ENCRYPTION_KEY` | нет | нет |
|
||
| любой другой | нет | нет |
|
||
|
||
Список — **allowlist**, и это структурное решение, а не стилистическое.
|
||
Denylist требует, чтобы автор каждого нового ключа вспомнил про этот файл:
|
||
забытый ключ при denylist сразу публичен, при allowlist — сразу закрыт. Отказ
|
||
по умолчанию не зависит от внимательности.
|
||
|
||
Маршрут `GET /api/config/getConfig` **удалён целиком**: потребителей у него не
|
||
было ни одного, а фильтр на неиспользуемой двери — это по-прежнему дверь. Право
|
||
записи `HYSTERIA2_CONFIG_REMARK` тоже убрано: панель его только отображает.
|
||
|
||
Ключи оркестратора отклоняются отдельным сообщением, называющим владельца, —
|
||
«этим значением владеет оркестратор» это другой ответ, чем «такого ключа нет»,
|
||
и он ведёт оператора к `hy2xs-orchestrator reconfigure`.
|
||
|
||
Оба оставшихся экспорта формируются **в памяти** и отдаются прямо в ответ.
|
||
Раньше они шли через `os.Create` в `/var/lib/hy2xs-admin/export/`, и файл там
|
||
оставался навсегда — при `?includeSecrets=true` это означало расшифрованные
|
||
секреты пиров на диске, накапливающиеся с каждым нажатием кнопки. Каталога
|
||
`export/` больше не существует.
|
||
|
||
### Экспорт пиров: два режима, а не флаг
|
||
|
||
| Кнопка | Запрос | Что внутри |
|
||
| --- | --- | --- |
|
||
| **Экспорт настроек** | `POST /api/peer-export` | список пиров без секретов |
|
||
| **Резервная копия** | `POST /api/peer-export?includeSecrets=true` | то же плюс действующие секреты подключения |
|
||
|
||
Разница здесь продуктовая, а не техническая, и её нельзя оставлять неявной.
|
||
Записи с пустым секретом при импорте получают **новые** секреты. То есть
|
||
перенос обычным экспортом восстанавливает пиров, их квоты, лимиты и счётчики —
|
||
но все существующие клиентские ссылки после него перестают работать.
|
||
|
||
Раньше кнопка в панели была одна и всегда звала маршрут без `includeSecrets`,
|
||
хотя документация называла эту пару механизмом переноса пиров. Оператор
|
||
переносил пиров и обнаруживал, что все клиенты отвалились.
|
||
|
||
Резервная копия содержит фактические учётные данные доступа к VPN в открытом
|
||
виде, поэтому запускается только через явное подтверждение с описанием риска.
|
||
Такой файл следует хранить как пароль и удалять после завершения переноса.
|
||
|
||
### Импорт пиров
|
||
|
||
Импорт проверяется так же строго, как обычное создание пира: те же правила для
|
||
имени, quota, `maxDevices`, `disabled`, длины секрета. Дополнительно:
|
||
|
||
- неизвестные поля в JSON отклоняются, а не игнорируются молча;
|
||
- файл обязан содержать **ровно один** JSON-документ. `json.Decoder` читает
|
||
первый документ и останавливается, поэтому файл с хвостом принимался целиком,
|
||
а его вторая половина молча не применялась;
|
||
- партия проверяется целиком **до** первой записи в базу;
|
||
- применение идёт **одной транзакцией**;
|
||
- пир `bootstrap-admin-peer` защищён от перезаписи: его секрет продублирован
|
||
в `/etc/hy2xs/bootstrap-admin.secret`.
|
||
|
||
Транзакция — не дублирование проверки, а закрытие другого класса отказов.
|
||
Валидация проверяет содержимое файла и ничего не знает о том, что уже лежит в
|
||
базе. Пусть существуют `A(auth_id=aaa, name=alice1)` и
|
||
`B(auth_id=bbb, name=bob123)`, а файл несёт `(auth_id=aaa, name=bob123)`: поиск
|
||
найдёт A по `auth_id` и попытается переименовать её в `bob123` — прямо в
|
||
`UNIQUE(name)`. Пока записи применялись по одной, всё, что шло в файле до
|
||
конфликтной строки, оставалось применённым, и откатить это оператор уже не мог.
|
||
|
||
Криптоматериал (digest и шифртекст секретов) считается **до** открытия
|
||
транзакции: эти операции читают ключи из той же таблицы `config`, и держать на
|
||
ней открытую запись во время AES по каждой из тысяч записей незачем.
|
||
|
||
## Два слоя работы с конфигом 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://` ссылка содержит фактический тип обфускации, и совместимый клиент подключается по ней напрямую
|