fix(admin): закрыть обещания панели, которые продукт не выполнял
Девятый проход, по итогам приёмки v1.0.0-rc1 на живом Debian 13. Общая тема:
интерфейс обещал оператору то, что продукт умел, но до чего не доходило
управление.
Секрет пира. Подпись под полем предлагала оставить его пустым, сервер умел его
сгенерировать, и генерация была недостижима: в go-playground/validator тег
omitempty НЕ пропускает правило, если поле объявлено указателем и указатель не
nil — hasValue считает указатель на пустую строку «значением». Правило min=6
применялось к пустой строке и отказывало. Ловушка закрыта общим шагом
нормализации DTO, а не тегом на одном поле: та же ловушка ломала фильтр списка
пиров, где очищенный крестиком el-input отправляет `?name=`. Граница проходит по
каждому полю отдельно — у remark пустая строка означает «убрать пометку», у
disabled ноль означает «включён».
Отказы. Любая ошибка любого поля превращалась в слово `invalid`, а слой vo
определял код ответа СРАВНЕНИЕМ текста сообщения — тот же антипаттерн, который
запрещён панели, только на сервере. Ответ несёт errors[{code, field, message,
params}]; панель выбирает фразу по коду и подставляет причины под поля.
Сессия. Ветка «войдите заново» была недостижима дважды: сервер отвечает HTTP 200
на любой отказ, поэтому обработчик ошибок axios не вызывался, а условие в нём
проверяло code === "A0230" и поле msg, которых в этом API никогда не было.
Истёкший токен вдобавок уезжал с кодом системной ошибки.
Иконки. Контракт currentColor был объявлен в двух местах и не действовал: восемь
ассетов несли литеральный fill="#000000" на <path>, а атрибут представления
перебивает унаследованное CSS-свойство. Под это попадали все семь иконок
бокового меню на фоне #181818.
Имя пира. Два правила на одном поле противоречили друг другу (min=1 против
6-32), а копия набора символов в слое контроллеров несла неэкранированный дефис
и впускала `, - . / : ; <` — через панель проходило имя peer/name, которое
импорт того же пира отклонял. Набор символов ЛОГИНА сознательно не сужен и
закреплён тестом: он приходит из HY2XS_ADMIN_USER и оркестратором не
ограничивается.
Добавлены подпись «Разработано во Flamy» с адресом, принадлежащим приложению, и
контрактные тесты панели как обязательный шаг сборки. Их исполняет Bun, а не
vitest: jsdom не вычисляет currentColor и визуальной корректности не доказал бы,
зато vitest привёл бы в граф pnpm audit сотню транзитивных зависимостей.
docs/ разложена по слоям, 11-testing-and-acceptance.md (117 КБ) разбит на пять
частей, добавлен docs/acceptance/ с отчётом о прогоне rc1 и перечнем дефектов.
Обход документации в приёмке стал рекурсивным: плоский docs/*.md после
разнесения по каталогам совпадал бы ровно с одним файлом.
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
# Server Hysteria2 baseline
|
||||
|
||||
## Цель документа
|
||||
|
||||
Зафиксировать правила для серверного слоя Hysteria2 в модели, где UI поставляется вместе с проектом, а Hysteria берётся из official upstream во время установки.
|
||||
|
||||
## Роль Hysteria2
|
||||
|
||||
Hysteria2 — основной транспортный компонент сервера.
|
||||
Он не вендорится и не собирается как часть HY2XS.
|
||||
|
||||
## Source policy
|
||||
|
||||
Базовое правило:
|
||||
- Hysteria2 скачивается **во время установки**
|
||||
- источник — **официальный upstream**
|
||||
- install layer не должен подменять собой upstream-дистрибуцию Hysteria2
|
||||
|
||||
## Версионная политика
|
||||
|
||||
Ключевое правило: **«последняя стабильная» определяется на этапе сборки пакета, а не на целевом сервере.**
|
||||
|
||||
Не «HY2XS использует Hysteria vX.Y.Z», а:
|
||||
|
||||
> HY2XS по умолчанию берёт последний стабильный релиз Hysteria, доступный на момент сборки пакета. Разрешённая версия, URL артефакта и контрольная сумма замораживаются в получившемся install package.
|
||||
|
||||
Практика:
|
||||
|
||||
- builder обращается к каноническому upstream `HyNetworks/hysteria`;
|
||||
- принимаются только стабильные релизы с тегом вида `app/vX.Y.Z`, без draft и prerelease;
|
||||
- берётся ровно один артефакт `hysteria-linux-amd64`, URL используется в том виде, в каком его отдал upstream API;
|
||||
- SHA-256 считается локально от скачанного артефакта, а не берётся из стороннего файла;
|
||||
- версия, URL и SHA-256 фиксируются в metadata install package;
|
||||
- на target-сервере **никогда** не используется moving `latest`;
|
||||
- бинарник Hysteria2 устанавливается только на этапе `install`;
|
||||
- фактически установленная версия записывается в `post-install.env`;
|
||||
- `reconfigure` не обновляет и не откатывает бинарник Hysteria2.
|
||||
|
||||
Следствие: если между сборкой пакета и его установкой выйдет новая версия Hysteria, содержимое установки **не изменится под ногами**. Повторная установка старого пакета поставит ту же версию, что и в день сборки.
|
||||
|
||||
Переопределения builder:
|
||||
|
||||
```bash
|
||||
HYSTERIA_CHANNEL=stable # по умолчанию: разрешить последнюю стабильную
|
||||
HYSTERIA_CHANNEL=pinned # взять закоммиченный tools/build/hysteria-lock.env, без сети
|
||||
HYSTERIA_VERSION_OVERRIDE=v2.12.2 # закрепить конкретную версию
|
||||
```
|
||||
|
||||
## Compatibility gate
|
||||
|
||||
Автоматический выбор «последней стабильной» без проверки опасен: upstream может изменить схему конфигурации, и builder молча соберёт неработающий HY2XS.
|
||||
|
||||
Поэтому до создания release-пакета builder:
|
||||
|
||||
1. скачивает артефакт и сверяет SHA-256;
|
||||
2. сверяет `hysteria version` с разрешённой версией;
|
||||
3. рендерит канонический конфиг HY2XS **тем же кодом**, который работает на target-сервере;
|
||||
4. запускает реальный бинарник Hysteria с этим конфигом — отдельно для Gecko и для Salamander;
|
||||
5. только после этого формирует пакет.
|
||||
|
||||
Если upstream несовместим, ломается **сборка**:
|
||||
|
||||
```text
|
||||
BUILD FAILED: unsupported Hysteria stable v2.13.0
|
||||
```
|
||||
|
||||
а не production-сервер оператора.
|
||||
|
||||
## Платформа
|
||||
|
||||
- ОС: только Debian 13
|
||||
- init/system management: systemd
|
||||
- сетевой фильтр: nftables
|
||||
- архитектура baseline: x86_64/amd64
|
||||
|
||||
## Listen и сеть
|
||||
|
||||
### Listen
|
||||
- только IPv4
|
||||
- формат: `0.0.0.0:<PORT>`
|
||||
|
||||
### Порт
|
||||
- один фиксированный UDP-порт
|
||||
- этот порт должен совпадать в:
|
||||
- server config
|
||||
- firewall rules
|
||||
- `post-install.env`
|
||||
|
||||
## Обфускация
|
||||
|
||||
Новые установки HY2XS используют **Gecko**.
|
||||
|
||||
Gecko помечен upstream как **experimental**. Он достраивается поверх Salamander: помимо scramble он дополнительно фрагментирует QUIC handshake на пакеты случайного размера. HY2XS использует upstream-defaults размеров пакетов `512/1200` как проверенный production-профиль.
|
||||
|
||||
**Salamander остаётся полностью поддержанным режимом совместимости.** Смена типа обфускации требует соответствующих изменений на клиенте: это изменение wire-совместимости, а не косметическая настройка.
|
||||
|
||||
Baseline:
|
||||
|
||||
```yaml
|
||||
obfs:
|
||||
type: gecko
|
||||
gecko:
|
||||
password: "<сгенерированный пароль>"
|
||||
minPacketSize: 512
|
||||
maxPacketSize: 1200
|
||||
```
|
||||
|
||||
Режим совместимости:
|
||||
|
||||
```yaml
|
||||
obfs:
|
||||
type: salamander
|
||||
salamander:
|
||||
password: "<сгенерированный пароль>"
|
||||
```
|
||||
|
||||
Правила:
|
||||
- тип выбирается через `HY2XS_HYSTERIA_OBFS_TYPE` (`gecko` | `salamander`);
|
||||
- пароль должен быть сильным, генерируется автоматически при `__GENERATE__` или пустом значении;
|
||||
- пароль фиксируется в конфигурационном контуре и доступен оператору через runtime config и `post-install.env`;
|
||||
- `obfs`-блок формируется оркестратором целиком, а не собирается из отдельных placeholders внутри YAML — комбинация вида `type: gecko` рядом с блоком `salamander` структурно невозможна.
|
||||
|
||||
### Почему размеры пакетов Gecko не вынесены в env
|
||||
|
||||
Официальная схема `hysteria2://` передаёт только тип обфускации и пароль. `minPacketSize` и `maxPacketSize` в ссылку не помещаются.
|
||||
|
||||
Если разрешить оператору произвольные значения, сгенерированная клиентская ссылка перестанет полностью описывать подключение и потребуется отдельный формат — выгружаемый клиентский профиль. Пока такой задачи нет, фиксация `512/1200` даёт корректную ссылку и воспроизводимое поведение.
|
||||
|
||||
Валидация (на случай будущего расширения) централизована в оркестраторе: `min > 0`, `max >= min`, `max <= 2048`.
|
||||
|
||||
## Версия схемы конфигурации
|
||||
|
||||
```bash
|
||||
HY2XS_CONFIG_SCHEMA_VERSION=2
|
||||
```
|
||||
|
||||
Пакет понимает только свою версию схемы. Конфигурация с другой версией отклоняется fail-fast, а не применяется частично.
|
||||
|
||||
HY2XS `v1` **не мигрирует установки `0.x` на месте**: между `0.x` и `1.0.0` изменились схема конфигурации, тип обфускации по умолчанию и контракт выбора версии Hysteria. Переход выполняется чистой установкой.
|
||||
|
||||
## TLS
|
||||
|
||||
Требования:
|
||||
- production default: `acme`
|
||||
- поддерживаемые режимы: `acme | file | self_signed_dev`
|
||||
- `self_signed_dev` только для dev/lab и только при явном `HY2XS_ALLOW_SELF_SIGNED_DEV=true`
|
||||
- корректный `server_name` / SNI на клиентах
|
||||
- одна понятная TLS policy
|
||||
- без смешивания нескольких несовместимых схем по умолчанию
|
||||
|
||||
Инварианты:
|
||||
- `acme` -> только `acme` block в конфиге;
|
||||
- `acme` block обязан содержать `type: http|tls` из runtime env (`HY2XS_ACME_TYPE`);
|
||||
- `HY2XS_ACME_TYPE=dns` в production-профиле запрещён до отдельной реализации;
|
||||
- `HY2XS_HYSTERIA_AUTH_MODE` зафиксирован в `http` и валидируется fail-fast;
|
||||
- `HY2XS_HYSTERIA_OBFS_TYPE` принимает `gecko` (default) или `salamander` и валидируется fail-fast;
|
||||
- блок `masquerade` в baseline не задаётся: при включённой обфускации сервер и так перестаёт быть обычным HTTP/3 endpoint, поэтому masquerade не даёт выигрыша, а `404 Not Found` на обычный HTTP-трафик — ожидаемое поведение;
|
||||
- `ech` в baseline не включается: при включённой обфускации соединение целиком перестаёт выглядеть как обычный QUIC, поэтому ECH не даёт дополнительной выгоды (он полезен в bare-режиме);
|
||||
- `file` -> только `tls.cert`/`tls.key` block;
|
||||
- `self_signed_dev` -> только dev сценарии.
|
||||
|
||||
## Auth policy
|
||||
|
||||
Для baseline выбирается одна предсказуемая auth-модель.
|
||||
|
||||
Правила:
|
||||
- install flow должен оставить рабочий auth state
|
||||
- bootstrap auth material должен быть либо передан оператором, либо безопасно сгенерирован
|
||||
- дальнейшая модель выдачи доступа пользователям не фиксируется в этом пакете docs
|
||||
|
||||
## Bandwidth и congestion
|
||||
|
||||
Серверная baseline policy:
|
||||
- `bandwidth.up = 50 mbps`
|
||||
- `bandwidth.down = 50 mbps`
|
||||
- `bandwidth.disableLossCompensation = false`
|
||||
- `ignoreClientBandwidth = false`
|
||||
- `congestion.type = bbr`
|
||||
- `congestion.bbrProfile = standard`
|
||||
|
||||
Важно:
|
||||
- эти параметры сами по себе не исчерпывают speed policy;
|
||||
- корректный лимит ожидается только в паре с совместимым клиентским конфигом;
|
||||
- `congestion` — это **fallback** controller: он применяется, когда Brutal bandwidth не согласован сторонами. Подробнее — в [06-speed-limits-and-congestion.md](06-speed-limits-and-congestion.md).
|
||||
|
||||
## QUIC stateless reset
|
||||
|
||||
```yaml
|
||||
quic:
|
||||
disableStatelessReset: false
|
||||
```
|
||||
|
||||
Начиная с Hysteria 2.12.1 сервер отправляет stateless reset, чтобы клиент со stale-соединением после перезапуска сервера или сна устройства переподключался сразу, а не по таймауту. В 2.12.2 появилась возможность это отключить.
|
||||
|
||||
Для VPN-подобного применения HY2XS быстрый reconnect — плюс, поэтому механизм остаётся включённым, а значение фиксируется в конфиге явно.
|
||||
|
||||
## Возможности вне default-профиля
|
||||
|
||||
HY2XS обязан **понимать** современную схему Hysteria, но не обязан включать всё подряд. Разделяются три уровня:
|
||||
|
||||
| Возможность | Генерирует HY2XS | Читает и сохраняет | Отдельный профиль |
|
||||
| --- | :-: | :-: | :-: |
|
||||
| Gecko | да | да | — |
|
||||
| Salamander | fallback | да | — |
|
||||
| BBR / bbrProfile | да | да | — |
|
||||
| Loss compensation | да | да | — |
|
||||
| QUIC stateless reset | да | да | — |
|
||||
| ECH | нет | да | позже |
|
||||
| Mimic | нет | да | позже |
|
||||
| Realms | нет | да | позже |
|
||||
| Port hopping | нет | да | позже |
|
||||
| ACME DNS | нет | да | позже |
|
||||
| Masquerade | нет | да | позже |
|
||||
|
||||
Причина не в качестве этих возможностей, а в том, что каждая меняет соседнюю подсистему:
|
||||
|
||||
- **Mimic** — привилегии, eBPF/XDP, сторонний бинарник, требования к клиенту; текущий systemd-контракт намеренно запускает Hysteria под непривилегированным пользователем с `CapabilityBoundingSet=CAP_NET_BIND_SERVICE`, поэтому Mimic несовместим с ним по построению и требует отдельного security-профиля;
|
||||
- **Realms** — сетевая топология (STUN/hole punching вместо публичного IPv4 и own nftables);
|
||||
- **Port hopping** — nftables и capabilities; официально несовместим с Mimic;
|
||||
- **ECH** — жизненный цикл ключей и распространение конфигурации клиентам (Hysteria не генерирует ECH keypair сама);
|
||||
- **ACME DNS** — учётные данные провайдера и работа с секретами;
|
||||
- **Masquerade** — дополнительное web/proxy-поведение.
|
||||
|
||||
Ни одна из них не должна включаться toggle'ом, который незаметно меняет systemd capabilities или топологию firewall.
|
||||
|
||||
## Рекомендуемые пути
|
||||
|
||||
- `/etc/hysteria/config.yaml`
|
||||
- `/var/lib/hysteria/`
|
||||
- `/etc/hy2xs/hy2xs.env`
|
||||
- `/etc/hysteria/post-install.env`
|
||||
|
||||
## Серверные инварианты
|
||||
|
||||
После установки должно быть верно:
|
||||
|
||||
1. Hysteria2 получена из official upstream по замороженному в пакете URL и SHA-256
|
||||
2. фактическая версия совпадает с версией из metadata пакета и отражена в `post-install.env`
|
||||
3. конфиг валиден
|
||||
4. сервис стартует через systemd
|
||||
5. нужный UDP-порт реально слушается
|
||||
6. тестовый совместимый клиент может подключиться
|
||||
7. bundled UI работает поверх актуального состояния сервера
|
||||
8. `trafficStats.secret` отдельный от `JWT_SECRET`
|
||||
9. IPv6 listen не используется
|
||||
10. публичные клиентские endpoint/URL берутся из `HY2XS_PUBLIC_HOST` + `HY2XS_PUBLIC_PORT`, а не из `listen`/request-host
|
||||
11. сгенерированная `hysteria2://` ссылка содержит фактический тип обфускации и пароль, и совместимый клиент подключается по ней напрямую
|
||||
12. SNI в ссылке берётся из ACME-домена, затем из `HY2XS_DOMAIN`, затем из `HY2XS_PUBLIC_HOST`; IP-адрес как SNI не используется
|
||||
Reference in New Issue
Block a user