Предыдущий проход сделал правильным порядок «сначала долговременная запись, потом разрыв сессии» и правильно запретил откат при неудаче разрыва. Способа прийти к согласованному состоянию ПОТОМ он не дал: у двух операций повтор не работал вовсе. Импорт, заменивший auth_id: после неудавшегося /kick старое значение не хранится нигде, повтор того же файла читает из базы уже новое и рвёт его, а cron пропускал незнакомый authID молча — dao.ListPeer просто не возвращала строку. Живая сессия оставалась навсегда. Снижение maxDevices: повтор формы даёт 1 < 1 -> false, разрыва больше нет. Лимит устройств в политику доступа не входит и входить не должен — это свойство сессий, — поэтому механизма схождения у него не было. enforcePeerAccess стал сверкой живых сессий: обход идёт по каждому authID из /online. Нет строки в базе -> kick; peerAccessDenied -> kick; непригодный maxDevices -> kick; устройств больше разрешённого -> kick. Отказ базы при этом не рвёт ничего. Ни таблицы отложенных операций, ни очереди retry: список живых сессий уже есть, и это /online. Отдельно закрыт второй TOCTOU лимита устройств. Учёт выданных разрешений закрыл сравнение двух одинаковых снимков, но сетевой запрос выполнялся вне блокировки, поэтому снимки приходили в резервацию в произвольном порядке и устаревший откатывал lastOnline назад, возвращая уже занятое место. Это не data race — память защищена мьютексом, и -race здесь молчит принципиально. Последовательность «прочитать /online -> занять место» выполняется под замком по authId; глобальный замок не годится, внутри идёт сетевой запрос. Учёт разрешений больше не растёт бесконечно: запись снималась только на ветке отказа, поэтому в карте копились удалённые пиры и переписанные импортом идентификаторы. Уборка идёт по фактической картине подключений. Гейты приёмки доращены под все три инварианта и проверены в обе стороны. Go 1.26.7 -> 1.26.8. Документация приведена в соответствие в двух местах, где описывала снятую архитектуру. Разбор: docs/acceptance/2026-09-02-v1.0.0-rc3-preflight-findings.md
HY2XS Core
HY2XS — production‑установщик Hysteria2‑сервера с локальной HY2XS admin‑панелью, systemd‑юнитами, nftables‑firewall и воспроизводимой моделью release‑пакета для чистого Debian 13.
Что это · Возможности · Быстрый старт · Конфигурация · Версии · Сборка · Changelog · Лицензия
Что это
HY2XS — это install‑only пакет для развёртывания Hysteria2‑узла на чистом Debian 13 amd64. Проект разделяет сборку и установку: разработчик один раз собирает release‑архив на Debian 13 build‑хосте, а целевой сервер получает уже готовый пакет и запускает install.sh без git clone, go build, bun install, pnpm install и сборки frontend‑ассетов.
HY2XS подходит для сценария, где нужен один production‑сервер с Hysteria2, локальной админ‑панелью, управляемым firewall и предсказуемой конфигурацией. Проект не является универсальным server manager, update manager или панелью для любых Linux‑дистрибутивов.
Ключевая идея: на target‑сервере выполняется только установка готового пакета. Вся тяжёлая сборочная часть остаётся на build‑машине.
Возможности
HY2XS release‑пакет разворачивает и настраивает:
- официальный upstream‑бинарник Hysteria2: последняя стабильная версия выбирается при сборке пакета, закрепляется в его metadata и проверяется по SHA256;
- HY2XS admin — встроенную админ‑панель для управления users/peers, трафиком, конфигурацией, логами и состоянием сервера;
- systemd‑юнит
hysteria-serverдля Hysteria2; - systemd‑юнит
hy2xs-adminдля админ‑панели; - runtime‑конфиг
/etc/hy2xs/hy2xs.env; - Hysteria2‑конфиг
/etc/hysteria/config.yaml; - post‑install snapshot
/etc/hysteria/post-install.env; - bootstrap‑секреты администратора в
/etc/hy2xs/bootstrap-admin.secret; - nftables‑правила с staged apply и rollback guard;
- smoke‑проверки после установки;
- команды диагностики, статуса, реконфигурации и сбора support‑bundle.
HY2XS admin запускается локально по умолчанию на 127.0.0.1:8080. Наружу публикуется Hysteria2‑транспорт, а доступ к панели выполняется через SSH local port forwarding.
Архитектура
graph LR
operator[Оператор на Windows/Linux/macOS] -->|SSH 2323/tcp| ssh[sshd на target]
operator -->|SSH tunnel 127.0.0.1:8080| ui[HY2XS admin 127.0.0.1:8080]
internet[Интернет] -->|443/udp| hy2[Hysteria2 server]
letsencrypt[Let's Encrypt] -->|80/tcp при ACME http| acme[ACME challenge]
hy2 -->|HTTP auth localhost + machine token| ui
ui -->|trafficStats localhost| hy2
hy2 --> cfg["/etc/hysteria/config.yaml"]
ui --> env["/etc/hy2xs/hy2xs.env"]
fw[nftables] --> ssh
fw --> hy2
fw --> acme
В production‑профиле проект работает как IPv4‑only stack. IPv6 намеренно не входит в текущий baseline. Если у домена есть AAAA‑запись и включён строгий режим HY2XS_DNS_AAAA_POLICY=strict, установка останавливается до применения конфигурации.
Поддерживаемые платформы
Target‑сервер
| Компонент | Поддерживается |
|---|---|
| ОС | Debian 13 |
| Архитектура | amd64 / x86_64 |
| Init system | systemd |
| Firewall | nftables |
| Сетевой профиль | IPv4‑only |
| Hysteria2 target | linux‑amd64 |
| Admin UI | локальный bind по умолчанию: 127.0.0.1:8080 |
Build‑машина
| Компонент | Поддерживается |
|---|---|
| ОС | Debian 13 |
| Архитектура | amd64 / x86_64 |
| Git tree | обязателен |
| Dirty tree | запрещён по умолчанию |
| Интернет | нужен для apt, Go, Bun, Node.js, pnpm/npm registry, GitHub и upstream Hysteria2 |
Windows и macOS можно использовать для разработки, редактирования кода и работы с GitHub, но production‑архив должен собираться на Debian 13 amd64.
Что HY2XS не делает
Это важные границы проекта:
- не собирает проект на target‑сервере;
- не поддерживает Debian 12, Ubuntu, CentOS, Alpine и другие ОС;
- не поддерживает arm64 в текущем release‑профиле;
- не включает IPv6‑production baseline;
- не настраивает
sshdавтоматически; - не предоставляет полноценный uninstall/update framework;
- не обновляет Hysteria2 на уже работающем сервере:
reconfigureнамеренно не является Hysteria updater; - не мигрирует установки
0.xна1.0.0— переход выполняется чистой установкой, см. CHANGELOG; - не выполняет сложную миграцию старых неизвестных состояний сервера;
- не реализует Telegram‑бота, port hopping и универсальный access‑delivery workflow;
- не предназначен для установки поверх давно используемого сервера с неизвестными firewall/systemd‑правками.
Release‑пакет
Production‑сборка создаёт один архив:
hy2xs-install-<version>.tar.gz
Внутри архива находятся:
hy2xs-install/
├── install.sh
├── orchestrator/hy2xs-orchestrator
├── ui/hy2xs-admin/hy2xs-admin
├── config/hy2xs.env
├── templates/
├── systemd/
├── docs/
└── metadata/
При запуске install.sh пакет проверяет metadata/checksums.txt и выполняет
read‑only clean‑host preflight из распакованного архива. После этого он
передаёт управление install‑only orchestrator через exec — и больше не делает
ничего: сам install.sh не изменяет на сервере ни одного файла.
Всю раскладку выполняет уже оркестратор: ставит себя в
/usr/local/lib/hy2xs/hy2xs-orchestrator, создаёт symlink
/usr/local/bin/hy2xs-orchestrator, копирует package assets в
/usr/local/lib/hy2xs/package и продолжает установку. Это сделано ради одного
свойства: у изменений сервера ровно один владелец, поэтому при любом отказе
известно, что именно было создано и что откатывать.
Сетевая модель по умолчанию
| Назначение | Значение по умолчанию |
|---|---|
| SSH оператора | 2323/tcp |
| Hysteria2 | 443/udp |
| ACME HTTP challenge | 80/tcp, если HY2XS_TLS_MODE=acme и HY2XS_ACME_TYPE=http |
| HY2XS admin | 127.0.0.1:8080 |
| TrafficStats Hysteria2 | 127.0.0.1:36712 |
| Firewall mode | takeover в packaged baseline |
| Hysteria2 auth | http через локальный HY2XS admin |
| Hysteria2 obfs | gecko (512/1200); salamander доступен как режим совместимости |
| Congestion fallback | bbr, профиль standard |
| QUIC stateless reset | включён |
Важно: HY2XS_SSH_PORT нужен HY2XS для nftables‑правил и проверки доступности SSH‑порта. Сам sshd проект не перенастраивает. SSH на 2323 и вход только по ключу нужно настроить до запуска ./install.sh.
Версионная политика Hysteria2
HY2XS не привязан к конкретному номеру версии Hysteria.
Источник по умолчанию берёт последний стабильный релиз Hysteria, доступный на момент сборки пакета. Разрешённая версия, URL артефакта и контрольная сумма замораживаются в получившемся install‑пакете.
Как это работает:
build machine target server
───────────── ─────────────
определить последнюю стабильную ─┐
взять ожидаемый SHA-256 из │
upstream hashes.txt │
скачать артефакт и сверить его ├─► release‑пакет ──► скачать ровно
проверить, что бинарник принимает │ version + url этот артефакт,
канонический конфиг HY2XS │ + sha256 сверить SHA-256
заморозить version/url/sha256 ─┘ и `hysteria version`
Контрольная сумма берётся из upstream‑ассета hashes.txt, а не считается
только локально: локальный пересчёт подтверждает, что файл не изменился после
скачивания, но не доказывает, что скачан именно ожидаемый upstream artifact.
Что это даёт:
- новая установка получает актуальную Hysteria без ручного обновления version lock;
- если между сборкой пакета и его установкой выйдет новая версия, содержимое установки не изменится;
- повторная установка старого пакета поставит ту же версию, что и в день сборки;
- несовместимый upstream ломает сборку, а не сервер оператора.
Переопределения при сборке:
# по умолчанию: последняя стабильная
./tools/build/build.sh
# закрепить конкретную версию
HYSTERIA_VERSION_OVERRIDE=v2.12.2 ./tools/build/build.sh
# офлайн-сборка по закоммиченному tools/build/hysteria-lock.env
HYSTERIA_CHANNEL=pinned ./tools/build/build.sh
Фактически установленная версия видна в /etc/hysteria/post-install.env (HY2_VERSION), а способ её выбора — в HY2_RESOLUTION.
Обновление Hysteria на уже работающем сервере в текущем релизе не поддерживается: reconfigure намеренно не является Hysteria updater. Это сохраняет immutable‑контракт развёртывания.
Обфускация
Новые установки HY2XS используют Gecko.
Gecko помечен upstream как experimental. Он достраивается поверх Salamander: помимо scramble он дополнительно фрагментирует QUIC handshake на пакеты случайного размера. HY2XS использует upstream‑defaults размеров пакетов 512/1200 как проверенный production‑профиль.
Salamander остаётся поддержанным режимом совместимости. Смена типа обфускации требует соответствующих изменений на клиенте: это изменение wire‑совместимости, а не косметическая настройка.
| Gecko | Salamander | |
|---|---|---|
| Статус upstream | experimental | stable |
| Роль в HY2XS | default для новых установок | режим совместимости |
| Параметр | HY2XS_HYSTERIA_OBFS_TYPE=gecko |
HY2XS_HYSTERIA_OBFS_TYPE=salamander |
| В клиентской ссылке | obfs=gecko |
obfs=salamander |
Экспериментальность upstream остаётся контролируемым риском, потому что одновременно выполняются три условия: Salamander доступен как fallback, каждая разрешённая версия проходит compatibility gate до выпуска пакета, и существующие серверы никогда не переводятся на Gecko молча.
Размеры пакетов Gecko не выносятся в конфигурацию: официальная схема hysteria2:// не умеет их передавать, поэтому нестандартные значения сделали бы клиентскую ссылку неполной.
Быстрый старт для нового сервера
Ниже приведён полный путь для оператора, который работает с Windows и ставит HY2XS на чистый Debian 13 сервер.
В примерах используются условные значения:
| Параметр | Пример |
|---|---|
| IP сервера | SERVER_IP |
| Домен сервера | vpn.example.com |
| Email для ACME | admin@example.com |
| Release‑архив | hy2xs-install-0.2.2.tar.gz |
| SSH‑ключ | id_ed25519_hy2xs |
Замените эти значения на свои.
1. Проверьте DNS и внешние порты
До установки домен должен указывать A‑записью на IPv4‑адрес target‑сервера:
vpn.example.com -> SERVER_IP
Для стандартной установки с HY2XS_TLS_MODE=acme и HY2XS_ACME_TYPE=http снаружи должны быть доступны:
| Порт | Протокол | Назначение |
|---|---|---|
2323 |
TCP | SSH оператора |
80 |
TCP | ACME HTTP challenge |
443 |
UDP | Hysteria2 |
Если у домена есть AAAA‑запись, при строгой политике HY2XS_DNS_AAAA_POLICY=strict установка будет остановлена, потому что текущий production‑профиль HY2XS является IPv4‑only.
A‑запись должна указывать именно на этот сервер, а не просто существовать. Preflight сверяет её с публичными IPv4, назначенными интерфейсам машины, и останавливает установку при расхождении:
DNS IPv4 mismatch for HY2XS_PUBLIC_HOST vpn.example.com:
DNS A records: 185.xxx.xxx.10
server public IPv4: 185.xxx.xxx.27
Update the DNS A record before using this server.
Та же проверка выполняется в reconfigure и doctor, поэтому принудительная смена IPv4 провайдером не остаётся незамеченной. Адрес сервера определяется локально, без обращения к внешним сервисам определения IP.
Если сервер работает за NAT или на floating IP — это топология вне текущего baseline; осознанное решение оформляется значением HY2XS_PUBLIC_ENDPOINT_POLICY=warn.
2. Создайте SSH‑ключ на Windows
Откройте PowerShell:
ssh-keygen -t ed25519 -a 100 -f "$env:USERPROFILE\.ssh\id_ed25519_hy2xs" -C "hy2xs-target-1"
Выведите публичный ключ:
type "$env:USERPROFILE\.ssh\id_ed25519_hy2xs.pub"
Скопируйте всю строку вида:
ssh-ed25519 AAAA... hy2xs-target-1
Именно её нужно добавить на сервер в /root/.ssh/authorized_keys.
3. Войдите на сервер по паролю провайдера
Первый вход обычно выполняется по временному root‑паролю от провайдера:
ssh root@SERVER_IP
На сервере установите базовые пакеты:
apt-get update
apt-get install -y openssh-server sudo ca-certificates curl tar
4. Добавьте публичный ключ на сервер
На сервере замените пример ключа на свой реальный публичный ключ:
install -d -m 700 -o root -g root /root/.ssh
cat > /root/.ssh/authorized_keys <<'EOF'
ssh-ed25519 AAAA_REPLACE_WITH_YOUR_PUBLIC_KEY hy2xs-target-1
EOF
chmod 600 /root/.ssh/authorized_keys
chown -R root:root /root/.ssh
5. Переведите SSH на порт 2323 и отключите парольный вход
Не закрывайте текущую SSH‑сессию, пока не проверите новый вход по ключу.
Сначала посмотрите, какие SSH‑директивы уже заданы провайдером:
grep -RniE '^[[:space:]]*(Include|Port|PermitRootLogin|PasswordAuthentication|KbdInteractiveAuthentication|ChallengeResponseAuthentication|PubkeyAuthentication|AllowTcpForwarding|PermitOpen|GatewayPorts|Match)\b' \
/etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null || true
OpenSSH для многих параметров использует первое найденное значение. Поэтому поздний файл вида 99-*.conf может не перекрыть ранний provider‑конфиг. Безопаснее сначала закомментировать конфликтующие директивы, затем создать ранний 00-hy2xs-operator.conf.
for f in /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf; do
[ -f "$f" ] || continue
cp -a "$f" "$f.bak.$(date +%Y%m%d-%H%M%S)"
sed -i -E 's/^[[:space:]]*(Port|PermitRootLogin|PasswordAuthentication|KbdInteractiveAuthentication|ChallengeResponseAuthentication|PubkeyAuthentication|AllowTcpForwarding|PermitOpen|GatewayPorts|X11Forwarding|AllowAgentForwarding|MaxAuthTries|LoginGraceTime|ClientAliveInterval|ClientAliveCountMax)[[:space:]]+/# disabled-by-hy2xs-hardening: &/' "$f"
done
cat > /etc/ssh/sshd_config.d/00-hy2xs-operator.conf <<'EOF'
Port 2323
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PermitRootLogin prohibit-password
AllowTcpForwarding local
PermitOpen 127.0.0.1:8080 localhost:8080
GatewayPorts no
X11Forwarding no
AllowAgentForwarding no
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
EOF
sshd -t
systemctl reload ssh || systemctl restart ssh
Проверьте effective‑конфиг:
sshd -T | grep -E '^(port|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|permitrootlogin|allowtcpforwarding|permitopen|gatewayports) '
ss -H -ltn | grep -E ':(22|2323) '
Ожидаемо:
port 2323
permitrootlogin without-password
pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no
gatewayports no
allowtcpforwarding local
permitopen 127.0.0.1:8080 localhost:8080
В списке listen‑портов должен остаться 2323, а 22 не должен слушаться.
6. Проверьте вход по ключу
В новом PowerShell‑окне:
ssh -p 2323 -i "$env:USERPROFILE\.ssh\id_ed25519_hy2xs" root@SERVER_IP
Проверьте, что парольный вход запрещён:
ssh -p 2323 -o PreferredAuthentications=password -o PubkeyAuthentication=no root@SERVER_IP
Правильный результат:
Permission denied (publickey).
После этого можно дополнительно заблокировать пароль root на уровне Linux‑аккаунта:
passwd -l root
passwd -S root
Это не ломает вход по SSH‑ключу, но защищает от случайного возврата парольной SSH‑аутентификации в будущем.
7. Скопируйте release‑архив на сервер
С Windows:
scp -P 2323 -i "$env:USERPROFILE\.ssh\id_ed25519_hy2xs" .\hy2xs-install-0.2.2.tar.gz root@SERVER_IP:/root/
На сервере:
cd /root
tar -xzf hy2xs-install-0.2.2.tar.gz
cd /root/hy2xs-install
8. Создайте конфиг для своего сервера
Не редактируйте исходный шаблон внутри пакета без необходимости. Создайте отдельный source‑config:
cp config/hy2xs.env /root/hy2xs-target.env
nano /root/hy2xs-target.env
Минимально проверьте и измените:
HY2XS_DOMAIN=vpn.example.com
HY2XS_PUBLIC_HOST=vpn.example.com
HY2XS_ACME_EMAIL=admin@example.com
HY2XS_SSH_PORT=2323
HY2XS_UI_BIND_HOST=127.0.0.1
HY2XS_UI_PORT=8080
HY2XS_HYSTERIA_PORT=443
HY2XS_FIREWALL_MODE=takeover
HY2XS_FIREWALL_STAGED_APPLY=true
Для стандартной production‑установки оставьте:
HY2XS_IPV6_ENABLED=false
HY2XS_TLS_MODE=acme
HY2XS_ACME_TYPE=http
HY2XS_HYSTERIA_AUTH_MODE=http
HY2XS_HYSTERIA_OBFS_TYPE=gecko
HY2XS_UI_PUBLIC_ACCESS=false
Если нужен режим совместимости со старыми клиентами, укажите HY2XS_HYSTERIA_OBFS_TYPE=salamander. Подробнее — в разделе Обфускация.
9. Запустите установку
./install.sh --config /root/hy2xs-target.env --non-interactive
Установка идёт в две фазы с жёсткой границей между ними.
PHASE 0 — только чтение. До её успешного завершения на сервере не
изменяется ни один файл, включая /usr/local/lib/hy2xs:
- проверит, что запущено от root;
- проверит checksums release‑пакета;
- запустит clean‑host preflight из распакованного архива: платформа Debian 13 amd64, отсутствие предыдущей установки, валидность конфигурации.
PHASE 1 — применение изменений. Её целиком выполняет оркестратор, которому
install.sh передал управление через exec:
- установит сам себя в
/usr/local/lib/hy2xsи разложит runtime‑пакет; - создаст runtime‑каталоги и service users;
- запишет
/etc/hy2xs/hy2xs.env; - разложит bundled HY2XS admin;
- скачает закреплённый в пакете Hysteria2 binary из upstream, проверит SHA256 и фактическую версию;
- создаст
/etc/hysteria/config.yaml; - установит systemd‑юниты;
- применит nftables‑правила;
- выполнит smoke‑checks;
- зафиксирует успешное состояние в
/var/lib/hy2xs/install-state.json.
Если PHASE 0 не прошла, установщик завершается с ошибкой и сервер остаётся в том же состоянии, в котором был. HY2XS v1 не устанавливается поверх предыдущего поколения и не мигрирует его состояние: очистка старой установки — отдельная явная операция, см. docs/operations/14-legacy-cleanup.md.
10. Получите bootstrap‑пароль админки
cat /etc/hy2xs/bootstrap-admin.secret
Файл содержит:
ADMIN_USER=...
ADMIN_INITIAL_PASSWORD=...
ADMIN_CON_PASS=...
ADMIN_INITIAL_PASSWORD нужен для первого входа в HY2XS admin. После первого входа смените пароль через интерфейс.
ADMIN_CON_PASS — bootstrap‑секрет Hysteria2 peer/auth слоя. Не публикуйте этот файл и не отправляйте его в issue/log без редактирования.
11. Откройте HY2XS admin через SSH‑туннель
На локальной машине:
ssh -p 2323 `
-i "$env:USERPROFILE\.ssh\id_ed25519_hy2xs" `
-N `
-L 127.0.0.1:8080:127.0.0.1:8080 `
root@SERVER_IP
Пример для Linux/macOS (одной строкой):
ssh -p 2323 -i ~/.ssh/id_ed25519_hy2xs -N -L 8080:127.0.0.1:8080 root@SERVER_IP
После этого откройте в браузере:
http://127.0.0.1:8080/
Если порт 8080 на локальной машине занят, можно пробросить другой локальный порт:
ssh -p 2323 `
-i "$env:USERPROFILE\.ssh\id_ed25519_hy2xs" `
-N `
-L 127.0.0.1:18080:127.0.0.1:8080 `
root@SERVER_IP
И открыть:
http://127.0.0.1:18080/
12. Проверьте установку
На сервере:
systemctl status hysteria-server --no-pager
systemctl status hy2xs-admin --no-pager
ss -H -lun | grep ':443'
ss -H -ltn | grep ':8080'
ss -H -ltn | grep ':2323'
nft list ruleset
cat /var/lib/hy2xs/install-state.json
Выполните doctor‑проверку:
hy2xs-orchestrator doctor \
--package-dir /usr/local/lib/hy2xs/package \
--config /etc/hy2xs/hy2xs.env
Проверьте сводный статус:
hy2xs-orchestrator status \
--package-dir /usr/local/lib/hy2xs/package \
--config /etc/hy2xs/hy2xs.env
Конфигурация hy2xs.env
После установки основным редактируемым файлом является:
/etc/hy2xs/hy2xs.env
Менять runtime‑параметры нужно через этот файл и затем применять reconfigure. Редактирование post-install.env не меняет runtime‑состояние.
Основные параметры
| Переменная | Назначение | Значение по умолчанию в packaged baseline |
|---|---|---|
HY2XS_CONFIG_SCHEMA_VERSION |
Версия схемы конфигурации HY2XS. Конфигурация другой схемы отклоняется fail‑fast | 2 |
HY2XS_IPV6_ENABLED |
IPv6‑режим. В production baseline должен быть false |
false |
HY2XS_DOMAIN |
Домен для ACME и deploy‑профиля | fi.api.withen.pro |
HY2XS_DNS_AAAA_POLICY |
Поведение при наличии AAAA‑записи: strict, warn, off |
strict |
HY2XS_PUBLIC_ENDPOINT_POLICY |
Строгость проверки того, что A‑записи публичного endpoint ведут на IPv4 этого сервера: strict, warn, off |
strict |
HY2XS_PUBLIC_HOST |
Публичный host без схемы, порта и path | fi.api.withen.pro |
HY2XS_PUBLIC_PORT |
Публичный порт Hysteria2 endpoint | 443 |
HY2XS_SSH_PORT |
SSH‑порт, который будет разрешён firewall‑правилами | 2323 |
HY2XS_FIREWALL_MODE |
Режим управления nftables: managed, takeover, external, off |
takeover |
HY2XS_FIREWALL_STAGED_APPLY |
Включает staged apply и rollback guard firewall | true |
HY2XS_UI_BIND_HOST |
IPv4 bind HY2XS admin | 127.0.0.1 |
HY2XS_UI_PUBLIC_ACCESS |
Флаг публичного UI‑доступа. В baseline оставляйте false |
false |
HY2XS_UI_PORT |
Порт HY2XS admin | 8080 |
HY2XS_ADMIN_USER |
Bootstrap‑логин администратора | hy2xsadmin |
HY2XS_ADMIN_INITIAL_PASSWORD |
Bootstrap‑пароль администратора; __GENERATE__ генерируется при install |
__GENERATE__ |
HY2XS_ADMIN_CON_PASS |
Bootstrap‑секрет peer/auth слоя; __GENERATE__ генерируется при install |
__GENERATE__ |
HY2XS_FORCE_PASSWORD_CHANGE |
Принудительная смена пароля. UX‑flow пока не включён в production baseline | false |
HY2XS_ALLOW_SELF_SIGNED_DEV |
Разрешает self_signed_dev TLS‑режим |
false |
HY2XS_TLS_MODE |
TLS‑режим: acme, file, self_signed_dev |
acme |
HY2XS_ACME_TYPE |
ACME challenge: http, tls, dns; dns пока не поддержан production‑профилем |
http |
HY2XS_ACME_EMAIL |
Email для ACME | admin@withen.pro |
HY2XS_TLS_CERT_PATH |
Путь к cert при file/self_signed_dev |
/etc/hysteria/server.crt |
HY2XS_TLS_KEY_PATH |
Путь к key при file/self_signed_dev |
/etc/hysteria/server.key |
HY2XS_HYSTERIA_BIND_HOST |
Bind Hysteria2. В production profile фиксируется на 0.0.0.0 |
0.0.0.0 |
HY2XS_HYSTERIA_PORT |
UDP‑порт Hysteria2 | 443 |
HY2XS_HYSTERIA_AUTH_MODE |
Auth mode Hysteria2. Фиксированное значение production‑профиля | http |
HY2XS_HYSTERIA_TRAFFIC_STATS_HOST |
Host trafficStats API | 127.0.0.1 |
HY2XS_HYSTERIA_TRAFFIC_STATS_PORT |
Порт trafficStats API | 36712 |
HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET |
Secret для trafficStats и machine auth | __GENERATE__ |
HY2XS_HYSTERIA_OBFS_TYPE |
Тип обфускации: gecko или salamander. Смена меняет wire‑совместимость |
gecko |
HY2XS_HYSTERIA_OBFS_PASSWORD |
Пароль обфускации; __GENERATE__ генерируется при install |
__GENERATE__ |
HY2XS_HYSTERIA_BANDWIDTH_UP |
Hysteria2 upstream bandwidth | 50 mbps |
HY2XS_HYSTERIA_BANDWIDTH_DOWN |
Hysteria2 downstream bandwidth | 50 mbps |
HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH |
Игнорировать bandwidth клиента | false |
HY2XS_HYSTERIA_CONFIG_PATH |
Путь Hysteria2 config. В production baseline фиксирован | /etc/hysteria/config.yaml |
HY2XS_INSTALL_DIR |
Каталог установки HY2XS admin | /opt/hy2xs-admin |
HY2XS_DATA_DIR |
Data‑каталог HY2XS admin | /var/lib/hy2xs-admin |
HY2XS_LOG_DIR |
Log‑каталог HY2XS | /var/log/hy2xs |
Режимы firewall
| Режим | Поведение |
|---|---|
managed |
HY2XS управляет nftables, но останавливается при обнаружении чужого /etc/nftables.conf |
takeover |
HY2XS явно берёт управление nftables на себя |
external |
HY2XS не меняет nftables; оператор сам отвечает за firewall |
off |
Firewall‑слой HY2XS отключён |
При managed и takeover генерируется nftables‑конфигурация с default drop policy, разрешением loopback, established/related, SSH‑порта, ACME challenge‑порта, Hysteria2 UDP‑порта и ICMP echo‑request.
Защита от потери доступа при смене firewall
При HY2XS_FIREWALL_STAGED_APPLY=true (значение по умолчанию) перед применением новых правил HY2XS взводит rollback guard — транзиентный systemd‑юнит с окном 45 секунд. Если операция не снимет его вовремя, guard вернёт прежний firewall, и SSH останется доступным.
Таймеру явно задаётся AccuracySec=1s, поэтому «45 секунд» — это реальный контракт, а не приблизительный: по умолчанию systemd.timer разрешает себе сработать в окне [цель; цель + AccuracySec], где AccuracySec — одна минута, и обещанное окно превращалось бы в 45–105 секунд. Вторым свойством задаётся RemainAfterElapse=no: отработавший таймер обязан выгрузиться, иначе он навсегда блокировал бы следующую операцию (см. «Одна операция за раз»).
Окно намеренно короткое и не обязано покрывать smoke‑checks: на медленном сервере они идут дольше. Вместо этого guard оставляет за собой факт срабатывания в /run/hy2xs/rollback/<op-id>/auto-rollback-fired, и операция не имеет права объявить себя успешной, если этот файл появился, — сервер в такой момент работает на прежнем firewall, а не на том, который она сгенерировала. Установка завершится отказом с phase: firewall_guard_fired, и её нужно повторить после устранения причины медленного прохода.
Дополнительно smoke сверяет, что действующий firewall — именно тот, который сгенерирован для текущей конфигурации: разбора /etc/nftables.conf для этого недостаточно, потому что прежний ruleset тоже валиден.
Одна операция за раз
install, reconfigure, repair и doctor сериализованы эксклюзивным замком /run/lock/hy2xs-orchestrator.lock. Вторая операция отказывает сразу и до первой мутации:
another HY2XS operation is already in progress: reconfigure (pid 4242, started at …)
Это не перестраховка: конфиги, unit‑файлы, /etc/nftables.conf и маркер установки — общие, и две одновременные операции записывают их поверх друг друга, после чего откат одной «восстанавливает» состояние поверх изменений другой.
status и diagnostics collect замок не берут — они нужны в том числе во время долгой операции, — но сообщают о ней в своём выводе.
Замок снимается сам при любом завершении держателя, включая Ctrl+C, SIGTERM и обрыв SSH. Если процесс был убит kill -9, следующая операция обнаружит мёртвого держателя и переиспользует замок самостоятельно.
Замка при этом недостаточно: он действует, пока жив процесс‑держатель, а rollback guard firewall — отдельный объект systemd, который свой процесс переживает. Аварийно умершая операция оставляет guard вооружённым, и он способен вернуть прежний firewall уже посреди следующей операции. Поэтому условие старта — не «предыдущая операция мертва», а «у неё не осталось исполнителей, способных изменить систему»:
previous HY2XS operation is no longer running, but its firewall rollback guard
is still armed: hy2xs-fw-rollback-<op-id>.timer (active/waiting)
Ждать в этом случае нужно не дольше 45–46 секунд с момента применения firewall.
Покоем считаются ровно два состояния юнита — inactive и failed: отработавший guard больше ничего не сделает, а отказ по failed заблокировал бы repair, которым чинят последствия. Всё остальное, включая незнакомые барьеру состояния systemd, операцию запрещает.
Отдельный случай — когда состояние guard'а вообще не удалось выяснить:
unable to verify firewall rollback guard state; systemd query failed,
refusing to start a lifecycle operation
Здесь ждать нечего: отсутствие ответа systemd — это отсутствие доказательства, а не доказательство покоя, и разбираться нужно с systemd. Барьер обязан доказать, что у предыдущей операции не осталось исполнителей, способных изменить firewall; молчаливое «наверное, всё в порядке» однажды означало бы срабатывание старого таймера поверх новой операции.
Реконфигурация
После изменения /etc/hy2xs/hy2xs.env сначала выполните dry‑run:
hy2xs-orchestrator reconfigure \
--package-dir /usr/local/lib/hy2xs/package \
--config /etc/hy2xs/hy2xs.env \
--dry-run
Если ошибок нет, примените изменения:
hy2xs-orchestrator reconfigure \
--package-dir /usr/local/lib/hy2xs/package \
--config /etc/hy2xs/hy2xs.env \
--apply
reconfigure --apply выполняет backup, генерирует конфиги, обновляет systemd‑юниты, применяет firewall, выполняет smoke‑checks и при ошибке пытается откатить изменённое состояние.
Важно: HY2XS_ADMIN_INITIAL_PASSWORD и HY2XS_ADMIN_CON_PASS являются install‑only bootstrap‑полями. Их изменение в /etc/hy2xs/hy2xs.env после первичной установки не ротирует уже созданные credentials в SQLite автоматически.
Команды управления
| Команда | Назначение |
|---|---|
hy2xs-orchestrator preflight-install |
Read‑only проверка чистоты хоста; ничего не меняет |
hy2xs-orchestrator status |
Показать состояние платформы, сервисов, firewall и install marker |
hy2xs-orchestrator doctor |
Выполнить preflight и smoke‑checks текущей установки; сервисы не перезапускает |
hy2xs-orchestrator reconfigure --dry-run |
Проверить конфиг без применения |
hy2xs-orchestrator reconfigure --apply |
Применить runtime‑конфигурацию |
hy2xs-orchestrator repair --allow-partial-state |
Довести до конца незавершённую установку текущего поколения |
hy2xs-orchestrator diagnostics collect |
Собрать diagnostic bundle в /var/log/hy2xs/diagnostics |
hy2xs-orchestrator redact-config |
Отредактировать секреты в env/yaml перед публикацией логов |
repair без --allow-partial-state работает только поверх полностью успешной
установки. В обоих режимах он сначала проверяет, что
/var/lib/hy2xs/install-state.json принадлежит текущему поколению продукта
(product, release_line, config_schema_version), и отказывается работать
поверх чужого состояния.
Пример сбора диагностики:
hy2xs-orchestrator diagnostics collect \
--package-dir /usr/local/lib/hy2xs/package \
--config /etc/hy2xs/hy2xs.env
Проверка безопасности после установки
Минимальный набор проверок:
# SSH слушает только ожидаемый порт
ss -H -ltn | grep -E ':(22|2323) '
# password auth отключён
sshd -T | grep -E '^(port|permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|allowtcpforwarding|permitopen) '
# UI не слушает публичный 0.0.0.0
ss -H -ltn | grep ':8080'
# Hysteria2 слушает UDP-порт
ss -H -lun | grep ':443'
# nftables-конфиг валиден
nft -c -f /etc/nftables.conf
# сервисы активны
systemctl is-active hysteria-server hy2xs-admin
Ожидаемые SSH‑значения:
port 2323
permitrootlogin without-password
pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no
allowtcpforwarding local
permitopen 127.0.0.1:8080 localhost:8080
Troubleshooting
Установка отказывается: обнаружена предыдущая установка
[hy2xs] ERROR: На сервере обнаружена предыдущая или посторонняя установка.
HY2XS v1 не поддерживает установку поверх и не мигрирует состояние 0.x.
Ни один файл на сервере не изменён.
Это ожидаемое поведение, а не сбой. Отказ происходит в PHASE 0, до любой мутации: сервер остался в том состоянии, в котором был.
Что делать:
- сохраните нужные данные (база пиров, конфиг) — см. docs/operations/14-legacy-cleanup.md;
- посмотрите план очистки:
sudo ./purge-v0.sh; - выполните очистку:
sudo ./purge-v0.sh --apply --yes-i-know; - повторите установку.
Отдельный случай — отказ вида
HY2XS_CONFIG_SCHEMA_VERSION отсутствует в конфигурации. Он означает, что
переданный --config относится к предыдущему поколению: до v1 этого поля не
существовало. Создайте конфиг заново по разделу «Создайте конфиг для своего
сервера».
Если установка HY2XS v1 упала после начала применения изменений, полная
очистка не нужна — используйте
hy2xs-orchestrator repair --allow-partial-state.
Установка падает на DNS AAAA
Причина: домен имеет IPv6 AAAA‑запись, а HY2XS production profile является IPv4‑only.
Решения:
- удалить AAAA‑запись у домена;
- либо временно установить
HY2XS_DNS_AAAA_POLICY=warn, если оператор осознанно принимает риск клиентских IPv6‑маршрутов вне текущего baseline.
DNS IPv4 mismatch: DNS ведёт не на этот сервер
Причина: A‑запись публичного endpoint указывает на адрес, которого нет среди публичных IPv4 этого сервера. Типичный случай — провайдер принудительно сменил IP, а DNS остался старым: сервисы на машине живы, но клиентская ссылка отправляет людей на другой адрес.
Решения:
- сверить фактический адрес сервера и обновить A‑запись:
ip -4 addr show scope global
- дождаться истечения TTL и повторить
hy2xs-orchestrator doctor; - если в строке
server public IPv4:пусто — на интерфейсах нет публичного IPv4 (сервер за NAT). Это вне baseline; при осознанном решении установитеHY2XS_PUBLIC_ENDPOINT_POLICY=warn.
Если A‑записей несколько и среди них есть посторонняя, проверка тоже отказывает: HY2XS — single‑host профиль, и второй backend за тем же именем означает, что часть клиентов попадёт не на этот сервер.
Установка падает на проверке SSH‑порта
HY2XS firewall‑слой проверяет, что порт из HY2XS_SSH_PORT уже слушается. Если указано 2323, но sshd продолжает слушать только 22, установка остановится.
Проверьте:
ss -H -ltn | grep -E ':(22|2323) '
sshd -T | grep '^port '
Вход по паролю всё ещё работает
Проверьте ранние provider‑конфиги:
grep -RniE '^[[:space:]]*(PermitRootLogin|PasswordAuthentication|KbdInteractiveAuthentication|PubkeyAuthentication)\b' \
/etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf
Если есть ранний файл с PasswordAuthentication yes, он может применяться раньше вашего hardening‑файла. Закомментируйте конфликтующие директивы и создайте 00-hy2xs-operator.conf, как показано в quick start.
UI не открывается в браузере
Проверьте, что SSH‑туннель запущен и сервис слушает localhost:
systemctl status hy2xs-admin --no-pager
ss -H -ltn | grep ':8080'
curl -sS http://127.0.0.1:8080/healthz
На локальной машине проверьте, что порт не занят другим приложением. При необходимости используйте локальный порт 18080 вместо 8080.
Нужно отправить логи без секретов
Соберите диагностику:
hy2xs-orchestrator diagnostics collect \
--package-dir /usr/local/lib/hy2xs/package \
--config /etc/hy2xs/hy2xs.env
Перед публикацией отдельных env/yaml файлов используйте redaction:
hy2xs-orchestrator redact-config \
--config /etc/hy2xs/hy2xs.env \
--out /root/hy2xs.env.redacted \
--format env
Сборка release‑пакета
Обычному пользователю не нужно собирать проект из исходников. Этот раздел нужен maintainer’у, который готовит release‑архив.
Перед работой: сверьте среду с контрактом
./tools/dev/doctor.sh # Linux/macOS
.\tools\dev\doctor.ps1 # Windows (PowerShell 7+)
HY2XS development environment
contract: versions.env (HY2XS 1.0.0, release line 1)
Go:
required: 1.26.8
found: 1.25.6
FAIL — локальный Go собирает не ту stdlib, что уедет в релиз; поставьте 1.26.8
Node:
required: 24.20.0
found: 24.20.0
OK
Скрипт ничего не устанавливает и не меняет — он отвечает на один вопрос: совпадает ли эта машина с контрактом сборки.
Раньше versions.env был контрактом только для сборки: она скачивает Go, Node и
Bun ровно тех версий, что там записаны, сверяя контрольные суммы, а машина
разработчика не проверялась никак. Расхождение обнаруживалось на Debian, внутри
release‑сборки, и выглядело как «у меня работало».
Расхождение не гипотетическое. Директива go в apps/go.mod — это языковой
baseline модуля, а не выбор компилятора, поэтому локальный Go другой минорной
линии собирал проект успешно, пока релизный бинарь компилировался на 1.26.7 и
наследовал её stdlib: проверялся не тот код, который уезжает в production.
Поэтому go.mod теперь объявляет toolchain явно, а doctor показывает
расхождение до сборки, а не после.
Сборка поддерживается на Debian 13 amd64 из чистого git work tree.
apt-get update
apt-get install -y git ca-certificates curl unzip tar xz-utils build-essential pkg-config bash coreutils findutils grep sed gawk openssl
git clone <repo-url>
cd <repo-dir>
git status --short
Подготовьте build env:
Версии и контрольные суммы toolchain не задаются переменными окружения: они
объявлены в корневом versions.env, и сборка берёт их оттуда.
Раньше их приходилось передавать снаружи, из-за чего воспроизводимая сборка в
чистой Debian‑среде требовала предварительного знания четырёх SHA‑256.
export BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ)
# Для переносимости между x86_64-серверами без AVX2 предпочтителен baseline artifact.
# Ожидаемый digest выбирается автоматически: в versions.env зафиксированы обе суммы.
export BUN_FLAVOR=x64-baseline
# Опционально: снимает anonymous rate limit при разрешении upstream-релиза.
export GITHUB_TOKEN=<token>
PACKAGE_VERSION тоже приходит из versions.env (HY2XS_VERSION). Шаг
verify_versions_contract роняет сборку, если версия продукта, схема
конфигурации, целевая платформа или packageManager в package.json
разошлись с контрактом.
Запустите сборку:
./tools/build/build.sh
Сборка последовательно:
- проверяет контракт
versions.env(verify_versions_contract); - прогоняет тесты и типы оркестратора (
bun test,tsc --noEmit); - прогоняет контрактные тесты панели (спрайт иконок, словари локализации, коды ошибок, атрибуция);
- определяет последнюю стабильную версию Hysteria, берёт ожидаемый SHA‑256 из upstream
hashes.txtи сверяет с ним скачанный артефакт; - проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS для Gecko и для Salamander;
- собирает orchestrator, frontend и backend, проставляя версию админки из контракта;
- прогоняет
go vetиgo testдля HY2XS admin; - проверяет граф зависимостей на известные уязвимости (
govulncheck ./...иpnpm auditпо всему lock‑графу); - формирует архив и прогоняет acceptance‑проверки.
Любой сбой на шагах 1–8 останавливает сборку до создания пакета.
Тесты и типы (шаги 2, 3 и 7) — такой же обязательный гейт, как проверка
зависимостей: переменной, которая их отключает, не существует. Готовый пакет
объявляет об этом полем tests_gate=true в metadata/package.env, и это
утверждение опирается на фактический прогон, а не на намерение.
Для локальной работы обходить нечего: bun test, bun x tsc --noEmit,
go vet ./..., go test ./... и
bun test tools/test/frontend-sprite.test.ts tools/test/frontend-contract.test.ts
запускаются напрямую и tarball не создают.
Переменные, управляющие выбором версии Hysteria:
| Переменная | По умолчанию | Назначение |
|---|---|---|
HYSTERIA_CHANNEL |
из versions.env (stable) |
stable — разрешить последнюю стабильную; pinned — офлайн‑сборка по tools/build/hysteria-lock.env |
HYSTERIA_VERSION_OVERRIDE |
пусто | Закрепить конкретную версию vX.Y.Z |
HYSTERIA_COMPAT_GATE |
true |
Compatibility gate; для release‑сборок обязателен |
HYSTERIA_VERIFY_UPSTREAM_HASHES |
true |
Сверять артефакт с upstream hashes.txt; отключение — только break‑glass |
HYSTERIA_WRITE_LOCK |
false |
Записать разрешённые значения обратно в lock‑файл |
Проверка зависимостей переменными не управляется: у неё нет аварийного
выхода. Релизный артефакт HY2XS невозможно собрать с непройденным гейтом, и
поле dependency_security_gate в metadata/package.env принимает единственное
значение true.
Раньше здесь были описаны два способа выпустить релиз, зная об уязвимости. Ими
они не являлись: финальная приёмка архива требует буквально
dependency_security_gate=true, поэтому сборка с любым из них проходила весь
цикл и падала на последнем шаге. Документированная операция, которую продукт сам
же запрещает, — хуже отсутствующей.
pnpm audit при этом проверяет весь lock‑граф frontend, а не только
production‑подграф. Причина в том, что build tooling исполняется на build‑машине
и порождает production‑бандл: уязвимость в vite/rollup уезжает в артефакт,
хотя сами они на сервер не копируются. Ровно такой случай и был найден — DOM
clobbering в Rollup затрагивал генерируемый бандл, а проверка по одному
production‑подграфу его не показывала.
Если advisory вышло в неудачный момент, чинится это обновлением графа
(apps/go.sum, apps/frontend/pnpm-lock.yaml) или версии toolchain в
versions.env. Для локальной работы обходить нечего: go test ./...,
govulncheck ./... и pnpm audit запускаются напрямую и tarball не создают.
Проверка versions‑контракта и проверка зависимостей отвечают на разные вопросы. Первая следит, что зафиксированные версии согласованы между собой; вторая — что про эти версии не стало известно плохого. Зафиксированный граф не стареет только на бумаге: advisory по нему выходят и после фиксации, а сборка релиза — единственный момент, когда это расхождение ловится дёшево.
Полный E2E с реальным клиентом Hysteria запускается отдельно (нужен Go: ссылка берётся из production‑генератора, а не из отдельной реализации внутри теста):
HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
Результат:
dist/hy2xs-install-<version>.tar.gz
Проверьте архив:
ls -lh dist/hy2xs-install-*.tar.gz
sha256sum dist/hy2xs-install-*.tar.gz
tar -tzf dist/hy2xs-install-1.0.0.tar.gz | grep -E \
'^(hy2xs-install/install.sh|hy2xs-install/orchestrator/hy2xs-orchestrator|hy2xs-install/ui/hy2xs-admin/hy2xs-admin|hy2xs-install/metadata/checksums.txt)$'
Подробная документация по сборочному слою находится в tools/build/README.md.
Структура репозитория
.
├── apps/ # HY2XS admin: Go backend + Vue/Vite frontend
├── orchestrator/ # install-only orchestrator на Bun + TypeScript
├── package/ # skeleton будущего install package
├── tools/build/ # production builder и packaging pipeline
├── tools/dev/ # doctor: сверка среды разработки с versions.env
├── tools/test/ # e2e с реальным клиентом Hysteria и контракты панели
├── tools/legacy/ # purge-v0.sh: очистка сервера от предыдущего поколения
├── docs/ # документация, разложенная по слоям
│ ├── architecture/ # baseline-модель и рамки
│ ├── build/ # builder layer и состав пакета
│ ├── runtime/ # оркестратор, systemd, post-install
│ ├── admin/ # HY2XS admin и контракты панели
│ ├── operations/ # runbook, разбор отказов, очистка 0.x
│ ├── testing/ # набор проверок по слоям
│ └── acceptance/ # отчёты о фактических прогонах приёмки
├── versions.env # контракт продукта, платформы и toolchain
├── CHANGELOG.md
├── README.md
└── LICENSE
Каталог dist/ создаётся builder’ом и не должен храниться в git.
Точка входа в документацию — docs/README.md.
Для кого этот проект
HY2XS рассчитан на операторов, которым нужен воспроизводимый способ поставить Hysteria2‑сервер с локальной панелью управления, не собирая проект на production‑сервере и не открывая admin UI наружу.
Проект особенно полезен, если важны:
- строгая target‑платформа;
- установка из одного release‑архива;
- локальный UI через SSH‑туннель;
- systemd/nftables baseline;
- проверяемая установка со smoke‑checks;
- понятная диагностика при сбоях.
Лицензия
HY2XS распространяется на условиях GNU Affero General Public License v3.0 only (AGPL-3.0-only).
Полный текст лицензии находится в LICENSE.
Сторонние библиотеки и зависимости сохраняют собственные лицензии.
Разработано во Flamy Studio.
