Разбор предыдущего прохода со сверкой по исходникам systemd v257.13 — той самой линии, что стоит на Debian 13. Тема та же и слоем глубже: контракт, объявленный шире, чем его принимает чужая сторона. Прошлый проход сделал транспорт lossless для значений, которые systemd принимает, но не спросил, какие значения он принимает вообще. 1. Домен значений файла окружения Перед тем как принять пару, systemd прогоняет ключ и значение через utf8_is_valid (src/basic/env-file.c, check_utf8ness_and_warn), и отказ там возвращает -EINVAL — то есть НЕзагруженный EnvironmentFile= и юнит, который не стартует, а не предупреждение. unichar_is_valid (src/basic/utf8.c) отвергает суррогаты, U+FDD0..U+FDEF и все code points вида *FFFE/*FFFF, а сам utf8_is_valid — встроенный NUL и невалидный UTF-8. Пароль "abcde" + U+FDD0 — шесть символов, восемь байт, ни одного управляющего — проходил панель, оркестратор, DTO и хеширование, записывался в hy2xs.env, и после этого админка не поднималась. Тот же класс дефекта, ради уничтожения которого контракт и существует, только слоем ниже. Введён IsEnvTransportableText (Go) / isEnvTransportable (TS), повторяющий множество systemd точно — не шире и не уже. Отдельно отвергаются одиночные суррогаты: строка JavaScript вправе их содержать, а TextEncoder молча заменяет непарный суррогат на U+FFFD, то есть без проверки в файл уехал бы ДРУГОЙ секрет, а не отказ. Заодно разделены домен транспорта и политика продукта. Проверка отвергала C0 и DEL с формулировкой «формат управляющих символов не несёт» — неправда: внутри двойных кавычек перевод строки накапливается как обычный байт и переживает round-trip. Именно эта подмена и позволила проверке не знать про noncharacters. Политика HY2XS теперь запрещает категорию Cc целиком (была шире кода ровно на C1) плюс U+FEFF — последний отдельным решением продукта, а не форматом: 0xFEFF & 0xFFFE это 0xFEFE, и systemd такое значение принимает. 2. Рецепт восстановления выполнял env-файл как код В docs/operations/12, раздел «Забыт пароль администратора», стояло `set -a; . /etc/hy2xs/hy2xs.env; set +a`. Строка стала опасной ровно тогда, когда файл научился нести произвольные значения. Для systemd HY2XS_ADMIN_INITIAL_PASSWORD="$(...)" — буквальное значение: подстановок в EnvironmentFile= нет вовсе. Но `.` обрабатывает файл bash, а bash внутри двойных кавычек выполняет подстановку команд — от root, прямо в рецепте восстановления доступа. Соседний раздел той же страницы при этом уже правильно запрещал source/eval для bootstrap-admin.secret: документ запрещал действие и тут же его предлагал. Рецепт читает нужные значения как ДАННЫЕ. Поставлен гейт приёмки, запрещающий возврат source/./eval над этими файлами в командах документации и в скриптах; гейт смотрит только внутрь ```-блоков, чтобы объяснение, называющее убранную конструкцию по имени, его не роняло. 3. Отказ приходил после мутаций хоста Проверка транспорта жила только внутри renderRuntimeEnv, то есть срабатывала на шаге «write runtime env» — уже после bootstrap оркестратора, установки пакетов и раскладки файловой системы, — а read-only preflight-install говорил PASS: он зовёт parseRuntimeEnv и ничего не рендерит. Детерминированно известная ошибка конфигурации роняла операцию, оставив за собой изменённый хост, что прямо противоречит контракту PHASE 0. validateRuntimeEnvTransport вызывается теперь из parseRuntimeEnv и проходит по ВСЕМ парам runtimeEnvEntries: ограничение принадлежит формату, а не полю пароля, и HY2XS_ADMIN_CON_PASS сломал бы загрузку юнита так же. 4. Точность порта автомата и его описания - в состоянии DOUBLE_QUOTE_VALUE_ESCAPE systemd пишет `c != '\n'`, а не проверку на любой перевод строки (в VALUE_ESCAPE — наоборот, strchr(NEWLINE, c)). Порт съедал и \<LF>, и \<CR>; - комментарий обещал одно намеренное расхождение с systemd, а их два: кроме строки без `=`, HY2XS отказывает и на незакрытой кавычке в конце файла. Оба fail-closed и теперь названы оба. Тесты: граничная таблица во всех слоях дополнена значениями вне домена (U+FDD0, U+FDEF, U+FFFE, U+FFFF, U+1FFFF, U+10FFFF, невалидный UTF-8), соседями диапазонов (U+FDCF, U+FDF0, U+FFFD, U+10FFFD), C1 и U+FEFF, одиночным суррогатом. Добавлены TestEnvTransportDomainMatchesSystemd (домен не шире и не уже) и TestProductPolicyIsWiderThanTransportDomain (домен и политика различимы), а также проверки fail-closed порядка: parseRuntimeEnv отвергает непригодную конфигурацию, проверяются все значения файла, запись и проверка ходят по одному списку пар. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HY2XS baseline docs
Этот набор документов фиксирует актуальную baseline-модель HY2XS под следующие ограничения:
- серверный транспорт: ванильная Hysteria2
- UI: HY2XS admin, штатный компонент проекта, поставляется вместе с пакетом
- target OS: только чистый Debian 13
- оркестратор: install-only, только первичная установка и базовая настройка
- стек оркестратора: Bun + TypeScript
- target-side build: запрещён
- standalone update / rollback / uninstall subcommands: вне scope
- сборка и упаковка: отдельный локальный build layer
- post-install state:
/etc/hysteria/post-install.env - установка: только на чистый хост, миграция с 0.x не поддерживается
- контракт версий продукта/платформы/toolchain: корневой
versions.env - клиентский delivery/access layer: вне baseline этого пакета docs
Главная архитектурная схема
В этой редакции зафиксированы два слоя:
-
Builder layer — работает на отдельном Debian 13 amd64 build host. Он собирает итоговый пакет, собирает HY2XS admin, компилирует Bun/TypeScript оркестратор в install-артефакт, упаковывает шаблоны, unit-файлы и примеры конфигов.
-
Runtime / target layer — работает на чистом Debian 13. Здесь нет сборщика. Здесь запускается только итоговый install package / orchestrator, который:
- ставит системные зависимости
- разворачивает встроенный HY2XS admin
- забирает закреплённую в пакете Hysteria2 из официального upstream и сверяет её по SHA-256 и версии
- создаёт конфиги, systemd unit-файлы и
post-install.env - выполняет базовую настройку сервера
Версия Hysteria2 выбирается на builder layer: последняя стабильная разрешается при сборке и замораживается в metadata пакета. Target layer никогда не обращается к moving latest.
Базовые правила
- Hysteria2 не вендорится и не собирается как часть HY2XS.
- HY2XS admin — штатный компонент проекта и поставляется вместе с пакетом.
- Оркестратор пишется на Bun + TypeScript.
- На target нет
npm/pnpm/yarn/bun install/ transpile step. - На target нет standalone логики update / rollback / uninstall.
- В install/reconfigure есть bounded rollback для failure-сценариев firewall/systemd/config/smoke. Rollback опирается на то, что операция реально успела применить: сервисы, которые она не разворачивала, не останавливаются никогда.
- Установка двухфазная: PHASE 0 — read only, PHASE 1 — mutation.
До успешного clean-host preflight на сервере не изменяется ни один
persistent path. У мутирующей фазы ровно один владелец — оркестратор:
install.shпроверяет и передаёт управление, не изменяя ничего сам. Очистка предыдущей установки — отдельная явная операция оператора, см. operations/14-legacy-cleanup.md. - Выдача доступа пользователям, Telegram-бот, billing, backend профилей и похожие контуры не входят в этот baseline.
Состав документов
Документы разложены по слою, к которому относятся. Двузначный префикс в имени — стабильный идентификатор документа: под ним на него ссылаются CHANGELOG, релизные гейты и сообщения оркестратора, поэтому при переносе в каталоги он сохранён.
Архитектура и рамки
- architecture/01-architecture-baseline.md — baseline-модель двух слоёв
- architecture/03-server-hysteria2.md — серверный транспорт
- architecture/05-client-and-access-scope.md — граница клиента
- architecture/06-speed-limits-and-congestion.md — ограничения скорости
- architecture/10-access-layer-out-of-scope.md — что вне baseline
Сборка
- build/02-build-layer-and-package.md — builder layer, состав пакета, требования к сборочной машине
Runtime на target
- runtime/08-orchestrator-spec.md — спецификация оркестратора
- runtime/07-systemd-and-firewall.md — systemd и nftables
- runtime/09-post-install-env.md — post-install состояние
Панель
- admin/04-admin-panel.md — HY2XS admin
- admin/15-ui-contracts.md — контракты панели: иконки, структурированные ошибки, необязательные поля, отсутствие выдуманного состояния, атрибуция
Эксплуатация
- operations/13-production-runbook.md — production runbook
- operations/12-operations-and-troubleshooting.md — операции и разбор отказов
- operations/14-legacy-cleanup.md — очистка установки предыдущего поколения
Проверки
- testing/ — набор проверок по слоям (бывший
11-testing-and-acceptance.md) - acceptance/ — отчёты о фактических прогонах приёмки
История изменений проекта — в CHANGELOG.md.
Жёсткие рамки baseline
Не делаем:
- upgrade manager
- rollback manager
- uninstall
- reconcile engine
- target-side build pipeline
- Docker baseline
- multi-node
- port hopping
- Telegram-бот
- backend выдачи remote profiles
- «умную» миграцию сломанных старых инсталляций
Одной фразой
Правильная baseline-модель теперь такая:
Локальный builder разрешает последнюю стабильную Hysteria2, проверяет её на совместимость с конфигом HY2XS и собирает install package с HY2XS admin и Bun/TypeScript оркестратором; серверный install-only orchestrator ставит этот пакет на чистый Debian 13, скачивает ровно закреплённую Hysteria2, разворачивает HY2XS admin, создаёт systemd + nftables + post-install env и подготавливает рабочее серверное окружение.