Files
HY2XS_flamy/CHANGELOG.md
T
founder 086b5d6624 build: закрепить новые инварианты приёмкой и документацией
verify_versions_contract получил сверку API namespace. Путь machine-auth
записывается в /etc/hysteria/config.yaml и в post-install.env, то есть по нему
Hysteria обращается к админке. Пока строка была продублирована в шаблонах,
smoke, тестах, приёмке и e2e, расхождение обнаруживалось только на живом
сервере. Теперь Go-константы, API_BASE фронтенда и оба шаблона сверяются
против значений, скомпилированных в оркестратор.

Приёмка проверяет, что:
  - fatal_pre_apply недостижим после записи install-state;
  - каждый ownership-флаг взводится раньше своего шага;
  - у read-only фазы нет универсального раннера, через который можно
    проскользнуть;
  - инвариант публичного endpoint живёт в preflight и не обращается к внешним
    сервисам определения IP;
  - purge-v0.sh и clean-host описывают одну границу;
  - секреты не попадают в персистентный файл экспорта;
  - импорт пиров валидируется так же строго, как их создание;
  - удалённые exportConfig/importConfig не вернулись.

Захардкоженная схема =2 в приёмке заменена на значение из versions.env: при
переходе на schema 3 пришлось бы помнить ещё и про эту строку.

Документация: контракт раннеров и ownership в 08, инвариант публичного
endpoint в 08/09/12/13 и README, сетевая идентичность панели и удалённые
export/import в 04, сценарии D1 (отказ сразу после PHASE 0) и D2 (устаревший
DNS после смены IPv4) в 11, версии package.json как не-версия продукта в 02.
2026-08-27 20:50:03 +05:00

389 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Изменения HY2XS
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/).
Проект использует [семантическое версионирование](https://semver.org/lang/ru/).
Версия относится к самому HY2XS, а не к Hysteria: версия Hysteria выбирается
на этапе сборки пакета и фиксируется в его metadata.
## [Unreleased]
Hardening-проход перед релизом `1.0.0`. Основная тема — сделать политику
«только чистая установка» настоящим системным инвариантом, а не строчкой в
документации.
### Исправлено
- **`fatal_pre_apply` мог означать «хост уже изменён».** `install-state.json`
пишется сразу после успешного preflight, до установки пакетов, но
классификация отказа его не учитывала. Падение `apt-get update` или
`apt-get install` объявлялось как «на сервере ничего не изменено»: откат и
обработка состояния пропускались, а маркер оставался на диске и ломал
следующую установку по clean-host контракту.
Ownership-флаги переформулированы с «шаг успешно завершился» на «операция
могла начать менять систему» и взводятся **перед** мутирующим вызовом:
`apt-get` умеет изменить систему и упасть. `fatal_pre_apply` теперь
недостижим ни при одном взведённом флаге, включая запись состояния.
- **Экспорт в админке оставлял секреты на диске навсегда.** `ExportPeer` и
выгрузка системного конфига шли через `os.Create` в
`/var/lib/hy2xs-admin/export/`, и файл там не удалялся. При
`?includeSecrets=true` это означало расшифрованные секреты пиров — фактические
учётные данные доступа — в открытом виде, накапливающиеся с каждым нажатием
кнопки. Экспорт формируется в памяти; каталога `export/` больше нет.
- **Generic export/import таблицы `config` выгружал и позволял подменить
криптографические ключи приложения.** Выгрузка исключала только сырой Hysteria
YAML, а в той же таблице лежат `JWT_SECRET`, `PEER_SECRET_KEY`,
`PEER_SECRET_ENCRYPTION_KEY` и `HYSTERIA2_TRAFFIC_STATS_SECRET`. Импорт их не
блокировал: подмена `PEER_SECRET_ENCRYPTION_KEY` ломает расшифровку секретов
уже существующих пиров. Оба маршрута и их UI удалены — production-сценария у
них не было, перенос пиров делают `peer-import`/`peer-export`.
- **Импорт пиров шёл мимо всей валидации.** Обычное создание пира проходит через
`dto.PeerSaveDto`, импорт JSON — нет: в базу попадало имя любой длины и с
любыми символами, `disabled` с произвольным числом, отрицательные счётчики.
Файл применялся построчно, поэтому ошибка в середине оставляла список пиров
наполовину изменённым, а импорт мог перезаписать `bootstrap-admin-peer`, чей
секрет продублирован в `/etc/hy2xs/bootstrap-admin.secret`. Партия теперь
проверяется целиком до первой записи, неизвестные поля отклоняются,
bootstrap-пир защищён.
- **DNS проверялся на существование A-записи, но не на то, куда она ведёт.**
После принудительной смены IPv4 провайдером `doctor` отвечал успехом, хотя
клиентская ссылка отправляла людей на чужую машину. Проверялся при этом
`HY2XS_DOMAIN`, тогда как в `hysteria2://` уезжает `HY2XS_PUBLIC_HOST`.
Добавлен инвариант публичного endpoint: A-записи обязаны принадлежать
множеству публичных IPv4, назначенных интерфейсам этого сервера. Проверка
живёт в общем `preflight`, поэтому действует в `install`, `reconfigure` и
`doctor`. Адрес определяется локально, без внешних сервисов определения IP.
Строгость управляется `HY2XS_PUBLIC_ENDPOINT_POLICY` (`strict` по умолчанию).
- **Read-only guard PHASE 0 можно было обойти.** Guard стоял на `writeText`,
`writeTextAtomic`, `runVisible`, `runHidden` и `runRawVisible`, но не на
универсальном `run`, через который в коде проходили и наблюдение (`ss`,
`systemctl is-active`), и настоящие мутации (`useradd`, `install -d`,
`mkdir`, `cp -a`, `tar`). Универсального раннера больше нет: есть
`runReadOnly*` без guard'а и `runMutating*` под guard'ом, а выбор — явное
решение на месте вызова.
- **Go-санитайзер конфига вырезал секреты из URL только у ключей `url`/`addr`.**
Будущее upstream-поле с другим именем (`endpoint:`) уносило встроенные
учётные данные и `access_token` наружу целиком; URL внутри списков не
обрабатывались вовсе. Граница определяется значением, а не именем ключа —
как в TS-санитайзере оркестратора; обе реализации покрыты зеркальными тестами.
- **`purge-v0.sh --keep-hysteria-binary` противоречил установщику.** Скрипт
сохранял `/usr/local/bin/hysteria` и сообщал «хост чист для установки
HY2XS v1», хотя clean-host контракт считает этот бинарник legacy-маркером и
следующая установка отказалась бы. Флаг удалён.
- **clean-host не замечал часть того, что удаляет purge.** `/var/lib/hysteria`
(ACME-состояние и сертификаты Hysteria), `/var/log/hy2xs`,
`/usr/local/lib/hy2xs` и `/usr/local/bin/hy2xs-orchestrator` не были
маркерами: сервер, где остался только старый runtime-state Hysteria, проходил
проверку и получал свежую установку поверх чужого состояния. Оба списка
теперь описывают одну границу, и приёмка это проверяет.
- **Установщик мог повредить работающий сервер до того, как откажется его
трогать.** `install.sh` переписывал `/usr/local/lib/hy2xs`, раскладывал
runtime-пакет и перезаписывал `/var/lib/hy2xs/install-state.json`, и лишь
потом запускал clean-host preflight. При ошибочном запуске поверх старого
сервера rollback дополнительно выполнял `stop` и `disable` для работающих
`hysteria-server` и `hy2xs-admin`.
Установка разделена на две фазы с жёсткой границей: **PHASE 0 — read only**,
**PHASE 1 — mutation**. Read-only проверка выполняется новой командой
`hy2xs-orchestrator preflight-install` из распакованного архива, а граница
держится runtime-guard'ом, а не соглашением.
- **Отсутствие `HY2XS_CONFIG_SCHEMA_VERSION` считалось текущей схемой.** До v1
этого поля не существовало, поэтому именно пустое значение — самый вероятный
признак конфигурации `0.x`. Теперь оно отклоняется как legacy с указанием на
чистую установку. Тест, закреплявший прежнее поведение, инвертирован.
- **`reconfigure` и `repair` работали поверх любого маркера установки.**
Проверялся только флаг `installed`, который мог остаться и от `0.x`.
Маркер получил идентификацию поколения (`product`, `release_line`,
`config_schema_version`), и обе команды проверяют её до всего остального.
- **Классификация отказа шла по тексту сообщения об ошибке.** Ошибка
preflight со словом `nftables` классифицировалась как отказ firewall и
приводила к откату чужого ruleset. Теперь классификация опирается на то, что
операция реально успела применить. `systemctl stop/disable` выполняется
только для юнитов, развёрнутых текущей операцией, а `fatal_pre_apply` по
определению не выполняет системный откат и не собирает diagnostics-бандл.
- **Diagnostics-бандл уносил machine token наружу.** Построчное правило
редакции `auth:` подставляло маркер в заголовок mapping'а и оставляло
нетронутым вложенный `auth.http.url` с `access_token=<секрет>` — тем самым,
что открывает и trafficStats API, и auth-endpoint. Редакция YAML переписана
структурно; в env-файлах секреты теперь вырезаются и из URL-значений
(`HY2_AUTH_URL` не подходил ни под один маркер имени).
- **`quic.maxIdleTimeout` не проверялся** семантической проверкой конфига, хотя
присутствовал в production-профиле. Заодно `auth.http.url` теперь сверяется
целиком (host/port/path/token), а не по наличию подстроки `access_token=`;
добавлены проверки `auth.http.insecure`, полей ACME и отсутствия посторонних
секций верхнего уровня.
- **Версия админки разъехалась с версией пакета**: пакет `1.0.0` сообщал
`HY2XS admin version v0.0.22`. Константа заменена переменной, которую
проставляет сборка через ldflags из `versions.env`.
- **Кнопки в панели, которые всегда возвращали ошибку.** «Перезапустить панель»
и загрузка сертификатов обращались к заглушкам. Маршруты и UI удалены.
### Добавлено
- **`versions.env`** — единственный источник истины для контракта
«продукт / платформа / toolchain»: версия продукта, линия релиза, схема
конфигурации, целевая платформа, версии и контрольные суммы Go/Bun/Node/pnpm,
политика выбора Hysteria. Прикладные зависимости и конкретная версия
Hysteria сюда намеренно не переносятся: у них есть собственные lock-механизмы.
- **Шаг сборки `verify_versions_contract`.** Роняет сборку до создания tarball,
если разошлись `PACKAGE_VERSION`, `packageManager` в двух `package.json`,
схема в `package/config/hy2xs.env`, константы, скомпилированные в
оркестратор, директива `go` в `apps/go.mod`, metadata пакета или версия,
которую сообщает собранный `hy2xs-admin`.
- **Контрольные суммы toolchain в контракте**, включая **обе** сборки Bun
(`bun-linux-x64` и `bun-linux-x64-baseline`): артефакт выбирается по наличию
AVX2, поэтому одной суммы архитектурно недостаточно. Передавать суммы через
окружение больше не нужно — production-сборка запускается одной командой.
- **Проверка происхождения артефакта Hysteria.** Ожидаемый SHA-256 берётся из
upstream-ассета `hashes.txt` и сверяется со скачанным бинарником до записи в
HY2XS lock. Раньше сумма считалась локально от уже скачанного файла, то есть
была trust-on-first-use.
- **Полный clean-host контракт.** Список маркеров чужой установки расширен с
двух до четырнадцати: состояние, runtime-пакет, конфиги, бинарник Hysteria,
фрагмент nftables, systemd-юниты, база админки и наследие `0.x`. Пути
установки и данных берутся из конфигурации, а не захардкожены.
- **`tools/legacy/purge-v0.sh`** и [docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md) —
явная очистка сервера от предыдущего поколения. По умолчанию скрипт
показывает план и ничего не делает; выполнение требует
`--apply --yes-i-know`. Из установщика он не вызывается никогда: это вернуло
бы destructive migration logic в путь свежей установки.
- **Явный флаг `--allow-partial-state` для `repair`.** Прежде согласие на
работу поверх незавершённой установки подразумевалось молча.
- **`HY2XS_PUBLIC_ENDPOINT_POLICY`** (`strict` | `warn` | `off`, по умолчанию
`strict`) — строгость проверки того, что публичный endpoint ведёт на этот
сервер. Ослабление предназначено для топологий вне baseline: NAT, floating IP,
anycast. Отсутствие A-записи фатально при любом значении.
- **Раздельные API подпроцессов в оркестраторе**: `runReadOnly` /
`runReadOnlySecret` для наблюдения и `runMutating*` под read-only guard'ом.
- **Сверка API namespace на сборке.** Путь machine-auth и базовый префикс
админского API объявлены по одной константе на компонент, а
`verify_versions_contract` сверяет Go, фронтенд и шаблоны против значений,
скомпилированных в оркестратор.
### Изменено
- **Пространства имён HTTP API.** Операторский и auth API переехали с `/hui` на
`/api`, machine-auth endpoint Hysteria — на `/internal/hysteria/auth`. Прежний
общий префикс был наследием H UI: под ним лежали и machine-to-machine auth, и
JWT-защищённый админский API, хотя middleware у них не пересекаются. Момент
выбран до первого clean-install релиза: после `1.0.0` эти строки стали бы
частью фактического v1 compatibility contract.
- **Сетевая идентичность админки принадлежит оркестратору.** Ключи
`H_UI_WEB_PORT`, `H_UI_WEB_CONTEXT`, `H_UI_CRT_PATH`, `H_UI_KEY_PATH` удалены
из схемы, seed и интерфейса вместе с собственным TLS-слоем панели. Раньше
оркестратор передавал порт аргументом, админка записывала его в SQLite и тут
же читала обратно, а UI показывал поля в disabled-виде: второй источник истины,
из которого ничего нельзя было изменить. Панель всегда монтируется в `/`.
- **`HUI_DATA`/`HUI_LOG``HY2XS_DATA_DIR`/`HY2XS_LOG_DIR`.** Мост в
systemd-юните, перекладывавший canonical env HY2XS в имена старого H UI,
удалён.
- **База админки — `hy2xs-admin.db`** вместо `h_ui.db`; reference-схема —
`apps/docs/sql/schema.sql` вместо `h_ui_db.sql`. Совместимость сохранять не
требуется: v1 ставится только с нуля. Историческое имя `h_ui.db` остаётся в
[docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md) — там это имя чужого
артефакта, который очистка должна найти.
- **Индикатор загрузки и legacy-цвета переведены на брендовый токен.**
NProgress приходил со своим `#29d` и был единственным элементом интерфейса вне
палитры HY2XS; страницы `401`/`404` и подсветка выбранной строки таблицы несли
цвета исходного admin-шаблона. Все они привязаны к `--el-color-primary`, а не
переписаны вторым литералом.
- **Приёмка сверяет схему конфигурации с `versions.env`**, а не с числом `2`
в тексте проверки.
- **E2E подключается по ссылке из production-кода.** Внутри
`tools/test/e2e-hysteria.sh` жила вторая реализация `hysteria2://` URI на
bash: дрейф любой из двух реализаций оставлял обе группы тестов зелёными.
Теперь ссылку выдаёт `service.BuildHysteria2ShareURI` через
`apps/tools/share-uri`. Единственное расхождение — `insecure=1` для
самоподписанного сертификата, и оно ограничено тестами с двух сторон.
- **Формулировка гарантии санитайза экспорта.** Вместо «любой будущий секрет
будет удалён» — «известные секреты и неизвестные поля с секретоподобным
именем». Список маркеров расширен (`apiKey`, `privateKey`, `authorization`,
`cookie`, `bearer`, `passphrase`, `signature`, …) и синхронизирован между
Go-админкой и оркестратором.
### Удалено
- Маршруты, операциями которых продукт не владеет:
`POST /hysteria2ChangeVersion`, `GET /listRelease`,
`POST /config/updateHysteria2Config`, `POST /config/importHysteria2Config`,
`POST /config/restartServer`, `POST /config/uploadCertFile`,
`GET /config/hysteria2AcmePath`. Вместе с ними — соответствующие сервисы,
клиентские функции фронтенда, кнопки и строки i18n.
Маршруты удалены, а не оставлены отвечающими «feature disabled»: API-контракт
не должен обещать updater, которого у продукта нет, а неиспользуемый маршрут
остаётся attack surface.
- `POST /config/exportConfig` и `POST /config/importConfig` — generic-выгрузка и
загрузка таблицы `config` вместе с криптографическими ключами приложения.
Вместе с ними — кнопки Import/Export в настройках, клиентские функции и
строки i18n.
- Персистентный каталог выгрузок `/var/lib/hy2xs-admin/export/` и
файловый helper `util.ExportFile`. Артефакт, который покидает сервер, не
должен существовать на сервере дольше самого запроса.
- Флаг `purge-v0.sh --keep-hysteria-binary`.
- Мёртвые строки i18n, оставшиеся от H UI: `noHttpsTip`, `defaultPassTip`,
`hui*`, `useHysteria2Cert`, `invalidWebContext`, `mustBeInteger`.
## [1.0.0] — 2026-08-27
Первый релиз линейки `v1`.
> **Обновление с `0.x` не поддерживается.**
> Между `0.x` и `1.0.0` изменились схема конфигурации, тип обфускации
> по умолчанию и контракт выбора версии Hysteria. Сервер, установленный
> из пакета `0.x`, нужно поднимать заново: очистка и установка с нуля.
> Подробности — в разделе «Миграция с 0.x» ниже.
### Добавлено
- **Разрешение версии Hysteria на этапе сборки.** Builder по умолчанию сам
определяет последний стабильный upstream-релиз, скачивает артефакт,
вычисляет SHA-256 и замораживает `version` + `url` + `sha256` в metadata
пакета. Target-сервер по-прежнему скачивает конкретный неизменяемый
артефакт и никогда не обращается к moving `latest`.
- **Compatibility gate в сборке.** До создания release-пакета builder
рендерит канонический конфиг HY2XS тем же кодом, что и оркестратор, и
запускает с ним реальный бинарник Hysteria — для обоих профилей
обфускации. Несовместимый upstream ломает сборку, а не сервер оператора.
- **Поддержка Gecko-обфускации** (Hysteria 2.9.2+) со сквозной интеграцией:
оркестратор, шаблон конфига, модель админки, генерация `hysteria2://` URI,
типы и формы фронтенда.
- **Версия схемы конфигурации** `HY2XS_CONFIG_SCHEMA_VERSION=2`. Пакет
отказывается работать с конфигурацией неизвестной схемы вместо того,
чтобы молча применить чужие значения.
- **Современный серверный baseline** в генерируемом конфиге:
`congestion.type: bbr` + `bbrProfile: standard`,
`bandwidth.disableLossCompensation: false`,
`quic.disableStatelessReset: false`, а также
`maxIdleTimeout`, `maxIncomingStreams`, `disablePathMTUDiscovery`.
- **Модель современной схемы Hysteria в админке**: `obfs.gecko`, `ech`,
`congestion`, `mimic`, `realm`, `tls.clientCA`,
`quic.disableStatelessReset`, `bandwidth.disableLossCompensation`,
`masquerade.proxy.xForwarded`. Поля читаются и отображаются, даже если
HY2XS не включает их в default-профиль.
- **Тесты оркестратора** (`bun test`): разбор env, рендер конфига,
семантические инварианты профиля, резолвер upstream-релизов,
release rollover.
- **E2E-проверка с реальным клиентом Hysteria** — `tools/test/e2e-hysteria.sh`:
TLS и obfs handshake, HTTP auth (допуск и отказ), TCP и UDP forwarding,
trafficStats, per-peer accounting, переподключение после перезапуска
сервера и подключение клиента именно по сгенерированной ссылке.
- **`CHANGELOG.md`** в корне репозитория.
### Изменено
- **Обфускация по умолчанию для новых установок — Gecko.** Salamander
остаётся полностью поддержанным режимом совместимости и выбирается через
`HY2XS_HYSTERIA_OBFS_TYPE=salamander`.
- **Gecko использует upstream-defaults `512/1200`** и не выносит размеры
пакетов в env: официальная URI-схема не умеет их передавать, поэтому
нестандартные значения сделали бы клиентскую ссылку неполной.
- **Тип обфускации больше не собирается внутри статического YAML.**
Оркестратор формирует проверенный `obfs`-блок целиком, поэтому
комбинация вида `type: gecko` рядом с блоком `salamander` структурно
невозможна.
- **Экспорт конфига Hysteria работает от исходного YAML**, а не от
типизированной модели: поля, о которых HY2XS ещё не знает, переживают
выгрузку.
- **Smoke-проверки разбирают YAML** и сверяют его с production-профилем,
вместо поиска подстрок.
- **Канонический upstream-репозиторий** — `HyNetworks/hysteria`
(вместо устаревших ссылок на `apernet`).
- **Реестр ACME DNS-провайдеров во фронтенде** приведён к актуальному
upstream: добавлены `namecheap`, `njalla`, `porkbun`.
- **Дефолты формы Hysteria во фронтенде** отражают baseline HY2XS
(50/50 Mbps, Let's Encrypt, каталог ACME), а не пример из upstream-доки.
- Сборка запускает тесты оркестратора и админки до упаковки.
### Исправлено
- **`hysteria2://` для Gecko.** Генератор ссылок был завязан на
`Obfs.Salamander.Password`, поэтому при любой другой обфускации выдавал
формально корректную, но неработающую ссылку без параметров `obfs`.
- **SNI в клиентской ссылке при файловых сертификатах.** SNI брался только
из ACME-блока, поэтому при `HY2XS_TLS_MODE=file` уходил пустым. Теперь
источник — ACME-домен, затем `HY2XS_DOMAIN`, затем `HY2XS_PUBLIC_HOST`;
IP-адрес в качестве SNI не используется.
- **Утечка секретов в экспорте конфига.** Выгружаемый оператору YAML
содержал `trafficStats.secret`, `access_token` в auth-URL и пароль
обфускации. Секреты вырезаются, включая поля, о которых HY2XS ещё
не знает.
- **Расхождение runtime-конфига с разобранным.** `renderRuntimeEnv`
печатал тип обфускации и режим auth литералами, игнорируя фактическую
конфигурацию, — из-за чего запись `/etc/hy2xs/hy2xs.env` могла разойтись
с тем, что реально применено.
- **Смешение веток `obfs` в UI.** Форма склеивала дефолт с ответом API и
показывала блок обфускации, которого нет в конфиге сервера.
- **`namedotcom` в списке ACME DNS-провайдеров.** Провайдер удалён из
Hysteria в 2.11.0; конфигурация с ним больше не запускается.
- **Отсутствие подсказок в форме создания пира.** У полей «Пир»,
«Комментарий» и «Секрет» не было ни примеров, ни пояснений: оператор не
мог понять без документации, что секрет необязателен и генерируется
автоматически.
### Безопасность
- Переход на Hysteria 2.12.2 закрывает исправления, вышедшие после 2.8.2,
включая обход UDP ACL, возможный OOM через sniff и обход ACL через
домены с завершающей точкой (2.9.2).
- Экспорт конфига больше не выносит секреты за пределы сервера.
### Миграция с 0.x
Автоматическая миграция не предусмотрена и не планируется.
Порядок перехода:
1. Выпишите с работающего сервера список пиров и их секреты.
2. Очистите сервер: `tools/legacy/purge-v0.sh` или ручная процедура из
[docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md).
3. Разверните `1.0.0` на чистом Debian 13 из release-пакета.
4. Заведите пиров заново и раздайте новые клиентские ссылки.
Установщик `1.0.0` обнаружит остатки предыдущей установки на шаге PHASE 0,
откажется работать и **не изменит на сервере ничего**.
Клиентские ссылки `0.x` в любом случае перестанут работать: смена
обфускации — это изменение wire-совместимости.
[Unreleased]: https://git.flamy.studio/prod/HY2XS_flamy/compare/v1.0.0...HEAD
[1.0.0]: https://git.flamy.studio/prod/HY2XS_flamy/releases/tag/v1.0.0