feat(v1): Gecko-обфускация, latest-stable Hysteria на сборке и forward-compatible admin

Сквозная миграция HY2XS на современную Hysteria (2.12.2) и переход на v1.

Build:
- версия Hysteria резолвится на этапе сборки из HyNetworks/hysteria и
  замораживается в metadata пакета (version + immutable url + sha256);
- compatibility gate: реальный бинарник должен принять канонический конфиг
  HY2XS для gecko и salamander до создания пакета;
- сборка прогоняет тесты оркестратора и админки.

Конфигурационный контракт:
- HY2XS_CONFIG_SCHEMA_VERSION=2, чужая схема отклоняется fail-fast;
- obfs стал настоящим union gecko|salamander, gecko — default;
- obfs-блок рендерится оркестратором целиком, два подтипа одновременно
  структурно невозможны;
- современный baseline: congestion bbr/standard, disableLossCompensation=false,
  disableStatelessReset=false, полный quic-блок.

Исправления:
- share URI для gecko: генератор был завязан на Obfs.Salamander.Password и
  выдавал нерабочую ссылку при любой другой обфускации;
- SNI брался только из ACME-блока и уходил пустым при HY2XS_TLS_MODE=file;
- экспорт конфига выносил trafficStats.secret, access_token и obfs-пароль;
- экспорт терял неизвестные upstream-поля при round-trip через типизированную
  модель;
- renderRuntimeEnv печатал тип обфускации литералом, расходясь с конфигом;
- namedotcom удалён из ACME-реестра (нет в Hysteria с 2.11.0).

Тесты:
- 95 тестов оркестратора: env, рендер, семантика профиля, резолвер, rollover;
- тесты URI и экспорта в Go;
- tools/test/e2e-hysteria.sh с реальным клиентом Hysteria.

UX:
- подсказки и примеры в форме создания пира.

Прочее: CHANGELOG.md, .gitattributes (LF для target-side файлов),
документация на русском.
This commit is contained in:
2026-08-27 08:15:02 +05:00
parent 0205334cd8
commit ddf0ddf71e
53 changed files with 4827 additions and 291 deletions
+3 -2
View File
@@ -41,9 +41,10 @@ Target layer **не содержит сборщика** и **не выполня
- **Hysteria2**
- QUIC/UDP
- один фиксированный UDP-порт
- `Salamander` включён по умолчанию
- `Gecko` включён по умолчанию, `Salamander` доступен как режим совместимости
- IPv4-only
- лимит по умолчанию: 50/50 Mbps на клиента
- fallback congestion controller: BBR (профиль `standard`)
### UI слой
- **HY2XS admin** — штатный компонент проекта
@@ -65,7 +66,7 @@ Target layer **не содержит сборщика** и **не выполня
## Принципы
### 1. Ядро, UI и оркестратор ведут себя по-разному
- Hysteria2: берём свежую upstream-версию при установке
- Hysteria2: последнюю стабильную upstream-версию выбирает **сборка пакета**, установка ставит уже замороженный артефакт
- HY2XS admin: разрабатываем **внутри проекта** и поставляем его сами
- Оркестратор: пишем на **Bun + TypeScript**, но собираем **локально**, а не на target
+81 -13
View File
@@ -43,13 +43,17 @@ Builder не является частью target install flow: на target serv
## Что делает builder
1. Проверяет структуру проекта.
2. Собирает / подготавливает HY2XS admin.
3. Компилирует оркестратор из Bun/TypeScript в install-артефакт.
4. Копирует артефакты UI в package staging directory.
5. Кладёт entrypoint, templates, docs и service files.
6. Формирует итоговый install package.
7. При необходимости считает manifest/checksum.
8. Выдаёт один переносимый результат для target machine.
2. Прогоняет тесты и типы оркестратора.
3. Разрешает upstream-версию Hysteria и проходит compatibility gate.
4. Компилирует оркестратор из Bun/TypeScript в install-артефакт.
5. Собирает / подготавливает HY2XS admin.
6. Прогоняет тесты HY2XS admin (после сборки frontend: `go:embed all:dist` требует готовых ассетов).
7. Копирует артефакты UI в package staging directory.
8. Кладёт entrypoint, templates, docs и service files.
9. Формирует итоговый install package.
10. Считает manifest/checksum.
11. Проверяет архив и прогоняет acceptance-проверки.
12. Выдаёт один переносимый результат для target machine.
## Что builder не делает
@@ -119,14 +123,76 @@ project/
Сам бинарь Hysteria2 **не вендорится** в install package как baseline-правило.
Причина:
- ядро Hysteria рассматривается как stable upstream component
- целевая установка должна брать его с official upstream на момент развёртывания
- ядро Hysteria рассматривается как stable upstream component;
- целевая установка скачивает его с official upstream, но **строго по замороженным координатам**.
### Разрешение версии на сборке
```text
SOURCE
resolve latest stable (HyNetworks/hysteria, только теги app/vX.Y.Z)
resolve exact release asset (hysteria-linux-amd64)
download + compute SHA-256
compatibility gate (реальный бинарник принимает канонический конфиг HY2XS)
PACKAGE METADATA
version = vX.Y.Z
exact_url = <immutable release asset>
sha256 = <...>
resolution = latest-stable | pinned | override
TARGET SERVER
скачивает уже конкретный неизменяемый артефакт
```
Так одновременно выполняются оба требования: «по умолчанию брать последнюю стабильную» и «production-установка должна быть детерминированной и проверяемой».
Переменные builder:
| Переменная | Значение по умолчанию | Назначение |
| --- | --- | --- |
| `HYSTERIA_CHANNEL` | `stable` | `stable` — разрешить последнюю стабильную через upstream API; `pinned` — взять `tools/build/hysteria-lock.env` без сети |
| `HYSTERIA_VERSION_OVERRIDE` | пусто | Закрепить конкретную версию `vX.Y.Z` |
| `HYSTERIA_COMPAT_GATE` | `true` | Compatibility gate; для release-сборок обязателен |
| `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в `tools/build/hysteria-lock.env` |
| `GITHUB_TOKEN` | пусто | Опционально, чтобы не упереться в anonymous rate limit |
Дополнительно:
- версия Hysteria2 фиксируется в metadata install package (`metadata/hysteria.version`);
- URL артефакта и SHA256 фиксируются в metadata install package (`metadata/hysteria.url`, `metadata/hysteria.sha256`);
- версия, URL и SHA256 фиксируются в metadata install package (`metadata/hysteria.version`, `metadata/hysteria.url`, `metadata/hysteria.sha256`);
- способ выбора версии фиксируется в `metadata/hysteria.resolution` и `metadata/package.env`;
- runtime `reconfigure` не обновляет и не откатывает бинарник Hysteria2;
- install flow валидирует SHA256 и фактическую версию установленного бинарника.
- install flow валидирует SHA256 и фактическую версию установленного бинарника;
- install-time код не обращается к upstream API и не использует moving `latest` — это проверяется тестами и acceptance-шагом сборки.
### Compatibility gate
Gate защищает от ситуации, когда upstream меняет схему конфигурации, а builder молча собирает неработающий HY2XS.
Порядок:
1. скачать артефакт и сверить SHA-256;
2. сверить `hysteria version` с разрешённой версией;
3. отрендерить канонический конфиг HY2XS тем же кодом, что работает на target (`orchestrator/tools/render-canonical-config.ts`);
4. запустить реальный бинарник Hysteria с этим конфигом — для Gecko и для Salamander;
5. только после этого собирать release package.
При несовместимости ломается сборка:
```text
BUILD FAILED: unsupported Hysteria stable v2.13.0
```
Это осознанно: ошибка должна проявиться на build machine, а не на сервере оператора.
## Инварианты
@@ -137,4 +203,6 @@ project/
3. на сервере нет отдельного build step
4. bundled UI уже находится внутри пакета
5. оркестратор authored as Bun/TypeScript, но на target приходит как готовый install-артефакт
6. Hysteria2 подтягивается install layer'ом с upstream, а не собирается на target из исходников
6. Hysteria2 подтягивается install layer'ом с upstream по замороженным координатам, а не собирается на target из исходников
7. выход новой версии Hysteria после сборки не меняет содержимое уже собранного пакета
8. несовместимый upstream ломает сборку, а не установку у пользователя
+146 -18
View File
@@ -18,13 +18,53 @@ Hysteria2 — основной транспортный компонент се
## Версионная политика
С учётом выбранной модели «берём свежее из upstream» фиксируется такая практика:
Ключевое правило: **«последняя стабильная» определяется на этапе сборки пакета, а не на целевом сервере.**
- по умолчанию install layer тянет **свежий upstream release / install source**
- фактически установленная версия обязательно записывается в `post-install.env`
- бинарник Hysteria2 устанавливается только на этапе `install`
- версия, URL и SHA256 фиксируются в metadata install package
- `reconfigure` не обновляет и не откатывает бинарник 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-сервер оператора.
## Платформа
@@ -48,14 +88,55 @@ Hysteria2 — основной транспортный компонент се
## Обфускация
В baseline включается:
- `obfs.type: salamander`
- `obfs.password`
Новые установки 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: "<сгенерированный пароль>"
```
Правила:
- пароль должен быть сильным
- пароль должен фиксироваться в конфигурационном контуре
- значение должно быть доступно оператору через runtime config и `post-install.env`
- тип выбирается через `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
@@ -72,8 +153,9 @@ Hysteria2 — основной транспортный компонент се
- `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` зафиксирован в `salamander` и валидируется fail-fast;
- блок `masquerade` в baseline не задаётся (допустимо, но приводит к `404 Not Found` на обычный HTTP трафик);
- `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 сценарии.
@@ -91,11 +173,55 @@ Hysteria2 — основной транспортный компонент се
Серверная baseline policy:
- `bandwidth.up = 50 mbps`
- `bandwidth.down = 50 mbps`
- `bandwidth.disableLossCompensation = false`
- `ignoreClientBandwidth = false`
- `congestion.type = bbr`
- `congestion.bbrProfile = standard`
Важно:
- эти параметры сами по себе не исчерпывают speed policy
- корректный лимит ожидается только в паре с совместимым клиентским конфигом
- эти параметры сами по себе не исчерпывают 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.
## Рекомендуемые пути
@@ -108,8 +234,8 @@ Hysteria2 — основной транспортный компонент се
После установки должно быть верно:
1. Hysteria2 получена из official upstream
2. фактическая версия отражена в `post-install.env`
1. Hysteria2 получена из official upstream по замороженному в пакете URL и SHA-256
2. фактическая версия совпадает с версией из metadata пакета и отражена в `post-install.env`
3. конфиг валиден
4. сервис стартует через systemd
5. нужный UDP-порт реально слушается
@@ -118,3 +244,5 @@ Hysteria2 — основной транспортный компонент се
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 не используется
+54 -1
View File
@@ -51,6 +51,54 @@ Target server **не собирает** admin-компонент из исход
HY2XS admin работает как надстройка над Hysteria YAML/API-слоем. Это нормально: важно только, чтобы источник истины по runtime-состоянию был понятен и не было двух конкурирующих конфигурационных миров без правил синхронизации.
Относительно конфигурации Hysteria панель **read-only**: конфиг генерирует оркестратор.
## Два слоя работы с конфигом 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`, `secret`, `token` или `credential`.
Последний пункт — обратная сторона сохранения неизвестных полей: новое upstream-поле с секретом вырезается ещё до того, как HY2XS про него узнает.
Пути к файлам (`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
@@ -81,7 +129,9 @@ HY2XS admin работает как надстройка над Hysteria YAML/AP
- склеивать unit Hysteria2 и unit HY2XS admin в один сервис;
- раздувать оркестратор из-за особенностей панели;
- использовать HY2XS admin как updater бинаря Hysteria2;
- использовать `JWT_SECRET` как `trafficStats.secret` для Hysteria API.
- использовать `JWT_SECRET` как `trafficStats.secret` для Hysteria API;
- экспортировать конфиг Hysteria через типизированную модель — так теряются неизвестные upstream-поля;
- выгружать конфиг с секретами в открытом виде.
## Что фиксировать в `post-install.env`
@@ -107,3 +157,6 @@ HY2XS admin работает как надстройка над Hysteria YAML/AP
5. Hysteria2 остаётся внешним vanilla upstream-компонентом
6. HY2XS admin не выступает updater-менеджером Hysteria2
7. `trafficStats.secret` не связан с `JWT_SECRET`
8. экспорт конфига сохраняет неизвестные upstream-поля
9. экспорт конфига не содержит секретов
10. сгенерированная `hysteria2://` ссылка содержит фактический тип обфускации, и совместимый клиент подключается по ней напрямую
+41 -4
View File
@@ -13,12 +13,27 @@
Это неверная модель.
## Три разные вещи, которые нельзя смешивать
Это главный источник путаницы в теме скоростей Hysteria.
| Механизм | Что это | Где задаётся |
| --- | --- | --- |
| **Политика HY2XS 50/50** | продуктовое решение проекта, сколько давать клиенту | `HY2XS_HYSTERIA_BANDWIDTH_UP` / `_DOWN` |
| **Brutal bandwidth** | режим Hysteria, работающий по согласованным сторонами значениям полосы | `bandwidth.up` / `bandwidth.down` на сервере + hints на клиенте |
| **Fallback congestion controller** | что делает Hysteria, когда Brutal не применяется | `congestion.type` / `congestion.bbrProfile` |
`50 mbps` здесь — **не** «оптимальная скорость Hysteria» и не свойство протокола. Это политика HY2XS.
## Что зафиксировано в baseline
### На сервере
- `bandwidth.up = 50 mbps`
- `bandwidth.down = 50 mbps`
- `bandwidth.disableLossCompensation = false`
- `ignoreClientBandwidth = false`
- `congestion.type = bbr`
- `congestion.bbrProfile = standard`
### На клиенте
Совместимый клиентский конфиг должен задавать соответствующие bandwidth hints:
@@ -29,13 +44,30 @@
Ожидаемый 50/50 Mbps contract считается корректным только тогда, когда сервер и клиентская конфигурация согласованы.
Логика выбора внутри Hysteria:
- когда стороны согласовали Brutal bandwidth — используется Brutal;
- когда это не применяется — используется выбранный fallback congestion controller.
Поэтому BBR тоже является частью явного baseline HY2XS, а не «настройкой по умолчанию, о которой можно не думать».
## Loss compensation
```yaml
bandwidth:
disableLossCompensation: false
```
Компенсация потерь (появилась в Hysteria 2.10.0) позволяет отправлять быстрее заданной полосы, чтобы компенсировать потерю пакетов. В baseline HY2XS она **включена**, а значение фиксируется в конфиге явно — проект про воспроизводимое поведение, а не про молчаливое следование upstream-дефолтам.
## Что делать с host-level BBR
`net.ipv4.tcp_congestion_control=bbr` можно оставить как общий системный тюнинг, но:
- это не главный механизм speed policy Hysteria2
- это не замена клиентским bandwidth hints
- это не центр документации по лимитам
- это не главный механизм speed policy Hysteria2;
- это не замена клиентским bandwidth hints;
- это **не то же самое**, что `congestion.type: bbr` в конфиге Hysteria — у Hysteria собственный congestion-control контур поверх QUIC;
- это не центр документации по лимитам.
## Что фиксировать в `post-install.env`
@@ -43,8 +75,11 @@
- `HY2_BANDWIDTH_UP`
- `HY2_BANDWIDTH_DOWN`
- `HY2_IGNORE_CLIENT_BANDWIDTH`
- `HY2_DISABLE_LOSS_COMPENSATION`
- `HY2_CONGESTION_TYPE`
- `HY2_BBR_PROFILE`
Дополнительно фиксируется `HY2_VERSION` как фактически установленная версия Hysteria2.
Дополнительно фиксируется `HY2_VERSION` как фактически установленная версия Hysteria2 и `HY2_RESOLUTION` как способ её выбора при сборке пакета.
## Что нельзя писать в проектных доках
@@ -52,6 +87,8 @@
- «лимит задаётся только на сервере, клиент не важен»
- «любой URI достаточно для полной speed policy»
- «host BBR и есть логика Hysteria»
- «50 mbps — оптимальная скорость Hysteria» (это политика HY2XS, а не свойство протокола)
- «Brutal и congestion controller — одно и то же»
## Правильная baseline-формулировка
+10 -2
View File
@@ -59,6 +59,7 @@
- `ORCH_ENTRYPOINT`
### Общие
- `HY2XS_CONFIG_SCHEMA_VERSION`
- `DEPLOY_DOMAIN`
- `PUBLIC_HOST`
- `PUBLIC_PORT`
@@ -71,7 +72,8 @@
### Hysteria
- `HY2_SOURCE=official-upstream`
- `HY2_VERSION`
- `HY2_VERSION` — фактически установленная версия
- `HY2_RESOLUTION` — как версия была выбрана при сборке: `latest-stable`, `pinned` или `override`
- `HY2_TLS_MODE`
- `HY2_ACME_EMAIL`
- `HY2_TLS_CERT_PATH`
@@ -81,11 +83,17 @@
- `HY2_AUTH_MODE`
- `HY2_AUTH_URL`
- `HY2_TRAFFIC_STATS_LISTEN`
- `HY2_OBFS_TYPE`
- `HY2_OBFS_TYPE``gecko` или `salamander`
- `HY2_OBFS_PASSWORD`
- `HY2_GECKO_MIN_PACKET_SIZE`
- `HY2_GECKO_MAX_PACKET_SIZE`
- `HY2_BANDWIDTH_UP`
- `HY2_BANDWIDTH_DOWN`
- `HY2_DISABLE_LOSS_COMPENSATION`
- `HY2_IGNORE_CLIENT_BANDWIDTH`
- `HY2_CONGESTION_TYPE`
- `HY2_BBR_PROFILE`
- `HY2_DISABLE_STATELESS_RESET`
- `HY2_CONFIG_PATH`
### HY2XS admin
+198
View File
@@ -4,6 +4,24 @@
Зафиксировать checklist для новой двухслойной схемы.
## Как запускать тесты
```bash
# Юнит-тесты и типы оркестратора
cd orchestrator && bun install --frozen-lockfile && bun run check && bun test
# Тесты и статический анализ HY2XS admin
cd apps && go vet ./... && go test ./...
# Полный E2E с реальным клиентом Hysteria (Debian 13 amd64)
HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
# Production-сборка: прогоняет тесты, резолвер и compatibility gate
./tools/build/build.sh
```
`build.sh` останавливается, если падают тесты оркестратора, тесты админки или compatibility gate.
## A. Builder layer tests
### Проверяем
@@ -16,6 +34,81 @@
7. builder сам доставляет отсутствующие build-зависимости
8. builder проверяет версии Go/Bun/Node.js/pnpm
9. builder пишет версии toolchain в metadata
10. builder прогоняет `bun test` и `go test` до упаковки
## A1. Latest-stable resolver
Фикстуры и ожидаемое поведение (`orchestrator/test/hysteria-release.test.ts`):
| Сценарий | Ожидание |
| --- | --- |
| stable `app/v2.12.2` | выбирается |
| prerelease `app/v2.13.0` | игнорируется |
| draft `app/v2.14.0` | игнорируется |
| чужое семейство тегов (`core/`, `docs/`) | игнорируется |
| тег без префикса `app/` | игнорируется |
| `app/v2.9.10` против `app/v2.9.2` | выбирается `2.9.10` (числовое сравнение, не строковое) |
| отсутствует `hysteria-linux-amd64` | ошибка |
| дублирующийся `hysteria-linux-amd64` | ошибка, а не случайный выбор |
| non-https URL артефакта | ошибка |
| невалидный semver в теге | игнорируется |
| пустой список релизов | понятная ошибка |
| несовпадение SHA-256 | сборка падает |
| сетевая ошибка / rate limit | понятная ошибка с подсказкой про `GITHUB_TOKEN` и `HYSTERIA_CHANNEL=pinned` |
Отдельно проверяется, что **`latest stable` — это именно stable, а не максимальная строка или самый свежий тег**.
## A2. Release rollover
Ключевой acceptance-критерий модели «latest на сборке»:
```text
Сегодня: latest = 2.12.2 → пакет A закрепляет 2.12.2
Завтра: latest = 2.12.3 → пакет B закрепляет 2.12.3
Повторная установка пакета A всё равно ставит 2.12.2
```
Проверяется на двух уровнях:
- резолвер даёт разный результат на разных снимках upstream (`orchestrator/test/release-rollover.test.ts`);
- install-time код не импортирует резолвер, не обращается к `api.github.com` и не использует moving `latest` — это утверждение проверяется тестом и acceptance-шагом сборки.
## A3. Compatibility gate
1. скачанный артефакт проходит проверку SHA-256;
2. `hysteria version` совпадает с разрешённой версией;
3. реальный бинарник принимает канонический конфиг HY2XS для Gecko;
4. то же для Salamander;
5. при несовместимости падает **сборка** с сообщением `BUILD FAILED: unsupported Hysteria stable vX.Y.Z`, а не установка у пользователя.
## A4. Конфигурационный контракт (unit)
Таблица `orchestrator/test/env.test.ts`:
| Вход | Ожидание |
| --- | --- |
| значение не задано | `gecko` |
| `gecko` | принято |
| `salamander` | принято |
| неизвестный тип | отклонено |
| `Gecko` (регистр) | отклонено |
| gecko `max < min` | отклонено |
| gecko `max > 2048` | отклонено |
| gecko `max == 2048` | принято |
| неположительный/нецелый `min` | отклонено |
| пустой obfs-пароль | автогенерация, а не пустое значение в конфиге |
| `HY2XS_CONFIG_SCHEMA_VERSION=1` | отклонено с указанием на чистую установку |
Отдельно — round-trip `parse(render(config)) == config`. Этот тест ловит класс ошибок «в рендер runtime-конфига попал литерал вместо значения из конфигурации».
Рендер конфига (`orchestrator/test/render-config.test.ts`):
- Gecko рендерит **только** gecko-подблок;
- Salamander рендерит **только** salamander-подблок;
- в конфиге никогда нет двух подтипов obfs одновременно;
- шаблон не содержит захардкоженного типа обфускации;
- пароль с пробелами и спецсимволами экранируется;
- YAML-инъекция через пароль отклоняется даже в обход env-валидации.
## B. Target install tests
@@ -56,6 +149,89 @@
21. `/etc/hysteria/config.yaml` имеет `0640 hysteria:hy2xs-admin`
22. `hy2xs-admin` может читать `/etc/hysteria/config.yaml`, но не может писать
## C1. Семантический smoke конфига
Недостаточно `grep` по YAML: он не отличит нужное поле от такой же строки в другой секции и не заметит оставшийся рядом лишний подблок.
Smoke разбирает `/etc/hysteria/config.yaml` и сверяет с production-профилем:
```text
effective Hysteria version == версия из metadata пакета
obfs:
type == HY2XS_HYSTERIA_OBFS_TYPE
ровно один подблок, соответствующий type
password непустой
для gecko: minPacketSize == 512, maxPacketSize == 1200
bandwidth:
up/down == runtime env
disableLossCompensation == false
congestion:
type == bbr
bbrProfile == standard
quic:
disableStatelessReset == false
окна и таймауты == baseline
trafficStats:
listen == runtime env
secret непустой
auth:
type == http
url содержит machine access_token
TLS:
acme-режим не содержит секции tls
file-режим не содержит секции acme
```
## C2. End-to-end с реальным клиентом
`tools/test/e2e-hysteria.sh`, отдельно для Gecko и Salamander:
1. сервер принимает сгенерированный конфиг и стартует;
2. TLS handshake;
3. handshake с обфускацией;
4. HTTP auth HY2XS: разрешённый пир принят;
5. HTTP auth HY2XS: неразрешённый пир отклонён;
6. клиент подключается **именно по сгенерированной `hysteria2://` ссылке**;
7. TCP forwarding;
8. UDP forwarding;
9. `trafficStats` с валидным secret;
10. `trafficStats` с невалидным secret отклоняется;
11. per-peer accounting содержит аутентифицированного пира;
12. перезапуск сервера;
13. быстрое переподключение клиента (поведение stateless reset).
Пункт 6 — тот самый, который ловит класс ошибок, неизбежный при наивном включении Gecko: сервер работает, ссылка формально валидна, а клиент по ней не подключается.
## C3. Share URI (unit)
`apps/service/hysteria2_api_test.go`:
- Gecko URI содержит `obfs=gecko` и `obfs-password`;
- Salamander URI содержит `obfs=salamander` и `obfs-password`;
- конфиг без обфускации даёт ссылку без `obfs`;
- неизвестный тип обфускации в ссылку не попадает;
- обфускация без пароля в ссылку не попадает;
- SNI: ACME-домен → `HY2XS_DOMAIN``HY2XS_PUBLIC_HOST`, IP не используется;
- спецсимволы в credentials и obfs-пароле переживают round-trip: `+`, пробел, `#`, `@`, `/`, `?`, `&`, `=`, `%`, кириллица;
- литеральный `+` кодируется как `%2B` и не схлопывается с пробелом (регрессия на upstream-баг 2.9.3).
## C4. Экспорт конфига (unit)
`apps/service/hysteria2_export_test.go`:
- неизвестные upstream-секции переживают экспорт целиком, включая вложенные карты и списки;
- операционные поля остаются читаемыми;
- вырезаются: obfs-пароль, `trafficStats.secret`, `access_token`, `auth.userpass`, учётные данные ACME DNS, пароли outbound;
- вырезается **неизвестное** поле с секретным именем;
- пути к файлам (`tls.key`, `ech.keyPath`, `clientCA`) остаются видимыми.
## D. Negative tests
1. не Debian 13
@@ -68,6 +244,9 @@
8. install flow прерван посередине
9. попытка использовать `HY2XS_IPV6_ENABLED=true`
10. `HY2XS_PUBLIC_HOST=0.0.0.0`
11. неизвестный `HY2XS_HYSTERIA_OBFS_TYPE`
12. конфигурация со схемой `HY2XS_CONFIG_SCHEMA_VERSION` из линейки `0.x`
13. upstream `latest` несовместим с шаблоном HY2XS — падает сборка, не установка
## E. Fix20 production matrix (обязательные сценарии)
@@ -119,3 +298,22 @@
13. production build verify падает, если `config/hy2xs.env` содержит placeholder-значения
14. production build verify падает при dirty git tree (кроме `ALLOW_DIRTY_BUILD=true`)
15. metadata содержит `source_git_commit`, `dirty_tree`, `build_profile=production`
16. builder без override на сегодняшний день автоматически выбирает последнюю стабильную версию Hysteria
17. собранный пакет содержит **точные** версию, URL и SHA-256
18. выход новой версии Hysteria после сборки не меняет содержимое старого пакета
19. новая установка генерирует Gecko
20. Gecko использует `512/1200`
21. установленная Hysteria реально принимает сгенерированный YAML
22. сервис запускается под существующим непривилегированным пользователем `hysteria`
23. созданный пользователь получает `hysteria2://` с `obfs=gecko` и `obfs-password`
24. совместимый клиент Hysteria подключается напрямую по этой ссылке
25. после перезапуска Hysteria клиент быстро восстанавливает соединение
26. режим `HY2XS_HYSTERIA_OBFS_TYPE=salamander` полностью работоспособен
27. admin читает Gecko-конфиг без ошибок
28. экспорт не уничтожает современные и неизвестные upstream-поля
29. экспорт не содержит секретов
30. frontend отображает Gecko
31. `namedotcom` удалён, актуальные ACME-провайдеры отражены
32. документация нигде не утверждает, что Salamander — фиксированный инвариант
33. документация не фиксирует конкретный номер версии как «текущую версию», а объясняет latest-stable build policy
34. форма создания пира содержит примеры значений и пояснения для полей «Пир», «Комментарий» и «Секрет»
+7 -2
View File
@@ -24,10 +24,12 @@
Здесь нет сборщика. Здесь запускается только итоговый install package / orchestrator, который:
- ставит системные зависимости
- разворачивает **встроенный HY2XS admin**
- забирает **свежую Hysteria2 из официального upstream**
- забирает **закреплённую в пакете Hysteria2 из официального upstream** и сверяет её по SHA-256 и версии
- создаёт конфиги, systemd unit-файлы и `post-install.env`
- выполняет базовую настройку сервера
Версия Hysteria2 выбирается **на builder layer**: последняя стабильная разрешается при сборке и замораживается в metadata пакета. Target layer никогда не обращается к moving `latest`.
## Базовые правила
1. Hysteria2 не вендорится и не собирается как часть HY2XS.
@@ -52,6 +54,9 @@
10. [10-access-layer-out-of-scope.md](10-access-layer-out-of-scope.md)
11. [11-testing-and-acceptance.md](11-testing-and-acceptance.md)
12. [12-operations-and-troubleshooting.md](12-operations-and-troubleshooting.md)
13. [13-production-runbook.md](13-production-runbook.md)
История изменений проекта — в [CHANGELOG.md](../CHANGELOG.md).
## Жёсткие рамки baseline
@@ -72,4 +77,4 @@
Правильная baseline-модель теперь такая:
**Локальный builder собирает install package с HY2XS admin и Bun/TypeScript оркестратором; серверный install-only orchestrator ставит этот пакет на чистый Debian 13, тянет свежую Hysteria2 из upstream, разворачивает HY2XS admin, создаёт systemd + nftables + post-install env и подготавливает рабочее серверное окружение.**
**Локальный builder разрешает последнюю стабильную Hysteria2, проверяет её на совместимость с конфигом HY2XS и собирает install package с HY2XS admin и Bun/TypeScript оркестратором; серверный install-only orchestrator ставит этот пакет на чистый Debian 13, скачивает ровно закреплённую Hysteria2, разворачивает HY2XS admin, создаёт systemd + nftables + post-install env и подготавливает рабочее серверное окружение.**