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:
@@ -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. форма создания пира содержит примеры значений и пояснения для полей «Пир», «Комментарий» и «Секрет»
|
||||
|
||||
Reference in New Issue
Block a user