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
+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 не используется