diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8304773 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +# Разработка ведётся в том числе на Windows, где часто включён core.autocrlf=true. +# Всё, что исполняется или читается на target-сервере Debian, обязано остаться LF: +# CRLF в shell-скрипте даёт `$'\r': command not found`, а в systemd unit и env-файле +# ломает разбор значений. + +* text=auto eol=lf + +*.sh text eol=lf +*.tpl text eol=lf +*.env text eol=lf +*.service text eol=lf +*.nft text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +*.sql text eol=lf + +# Бинарные ассеты не должны трогаться нормализацией. +*.png binary +*.ico binary +*.gif binary +*.jpg binary +*.woff binary +*.woff2 binary diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3554c11 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,129 @@ +# Изменения HY2XS + +Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/). +Проект использует [семантическое версионирование](https://semver.org/lang/ru/). + +Версия относится к самому HY2XS, а не к Hysteria: версия Hysteria выбирается +на этапе сборки пакета и фиксируется в его metadata. + +## [Unreleased] + +## [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. Разверните `1.0.0` на чистом Debian 13 из release-пакета. +3. Заведите пиров заново и раздайте новые клиентские ссылки. + +Клиентские ссылки `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 diff --git a/README.md b/README.md index 686fdc5..689f326 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ Возможности · Быстрый старт · Конфигурация · + Версии · Сборка · + Changelog · Лицензия

@@ -39,7 +41,7 @@ HY2XS подходит для сценария, где нужен один produ HY2XS release‑пакет разворачивает и настраивает: -- официальный upstream‑бинарник Hysteria2, закреплённый в metadata пакета и проверяемый по SHA256; +- официальный upstream‑бинарник Hysteria2: последняя стабильная версия выбирается при сборке пакета, закрепляется в его metadata и проверяется по SHA256; - HY2XS admin — встроенную админ‑панель для управления users/peers, трафиком, конфигурацией, логами и состоянием сервера; - systemd‑юнит `hysteria-server` для Hysteria2; - systemd‑юнит `hy2xs-admin` для админ‑панели; @@ -108,6 +110,8 @@ Windows и macOS можно использовать для разработки - не включает IPv6‑production baseline; - не настраивает `sshd` автоматически; - не предоставляет полноценный uninstall/update framework; +- не обновляет Hysteria2 на уже работающем сервере: `reconfigure` намеренно не является Hysteria updater; +- не мигрирует установки `0.x` на `1.0.0` — переход выполняется чистой установкой, см. [CHANGELOG](CHANGELOG.md); - не выполняет сложную миграцию старых неизвестных состояний сервера; - не реализует Telegram‑бота, port hopping и универсальный access‑delivery workflow; - не предназначен для установки поверх давно используемого сервера с неизвестными firewall/systemd‑правками. @@ -147,10 +151,74 @@ hy2xs-install/ | TrafficStats Hysteria2 | `127.0.0.1:36712` | | Firewall mode | `takeover` в packaged baseline | | Hysteria2 auth | `http` через локальный HY2XS admin | -| Hysteria2 obfs | `salamander` | +| 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‑пакете. + +Как это работает: + +```text +build machine target server +───────────── ───────────── +определить последнюю стабильную ─┐ +скачать артефакт, посчитать SHA-256 │ +проверить, что бинарник принимает ├─► release‑пакет ──► скачать ровно +канонический конфиг HY2XS │ version + url этот артефакт, +заморозить version/url/sha256 ─┘ + sha256 сверить SHA-256 + и `hysteria version` +``` + +Что это даёт: + +- новая установка получает актуальную Hysteria без ручного обновления version lock; +- если между сборкой пакета и его установкой выйдет новая версия, **содержимое установки не изменится**; +- повторная установка старого пакета поставит ту же версию, что и в день сборки; +- несовместимый upstream ломает сборку, а не сервер оператора. + +Переопределения при сборке: + +```bash +# по умолчанию: последняя стабильная +./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 сервер. @@ -379,10 +447,12 @@ HY2XS_IPV6_ENABLED=false HY2XS_TLS_MODE=acme HY2XS_ACME_TYPE=http HY2XS_HYSTERIA_AUTH_MODE=http -HY2XS_HYSTERIA_OBFS_TYPE=salamander +HY2XS_HYSTERIA_OBFS_TYPE=gecko HY2XS_UI_PUBLIC_ACCESS=false ``` +Если нужен режим совместимости со старыми клиентами, укажите `HY2XS_HYSTERIA_OBFS_TYPE=salamander`. Подробнее — в разделе [Обфускация](#обфускация). + ### 9. Запустите установку ```bash @@ -396,7 +466,7 @@ HY2XS_UI_PUBLIC_ACCESS=false 3. создаст runtime‑каталоги и service users; 4. запишет `/etc/hy2xs/hy2xs.env`; 5. разложит bundled HY2XS admin; -6. скачает pinned Hysteria2 binary из upstream и проверит SHA256; +6. скачает закреплённый в пакете Hysteria2 binary из upstream, проверит SHA256 и фактическую версию; 7. создаст `/etc/hysteria/config.yaml`; 8. установит systemd‑юниты; 9. применит nftables‑правила; @@ -507,6 +577,7 @@ hy2xs-orchestrator status \ | Переменная | Назначение | Значение по умолчанию в 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` | @@ -534,8 +605,8 @@ hy2xs-orchestrator status \ | `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` | Obfuscation type. Фиксированное значение production‑профиля | `salamander` | -| `HY2XS_HYSTERIA_OBFS_PASSWORD` | Salamander password | `__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` | @@ -717,7 +788,7 @@ git status --short Подготовьте build env: ```bash -export PACKAGE_VERSION=0.2.2 +export PACKAGE_VERSION=1.0.0 export BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) # Для переносимости между x86_64-серверами без AVX2 предпочтителен baseline artifact. @@ -728,6 +799,9 @@ export BUN_FLAVOR=x64-baseline export GO_ARCHIVE_SHA256= export NODE_ARCHIVE_SHA256= export BUN_ARCHIVE_SHA256= + +# Опционально: снимает anonymous rate limit при разрешении upstream-релиза. +export GITHUB_TOKEN= ``` Запустите сборку: @@ -736,6 +810,32 @@ export BUN_ARCHIVE_SHA256= ./tools/build/build.sh ``` +Сборка последовательно: + +1. прогоняет тесты и типы оркестратора (`bun test`, `tsc --noEmit`); +2. определяет последнюю стабильную версию Hysteria, скачивает артефакт и считает SHA‑256; +3. проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS для Gecko и для Salamander; +4. собирает orchestrator, frontend и backend; +5. прогоняет `go vet` и `go test` для HY2XS admin; +6. формирует архив и прогоняет acceptance‑проверки. + +Любой сбой на шагах 1–5 останавливает сборку до создания пакета. + +Переменные, управляющие выбором версии Hysteria: + +| Переменная | По умолчанию | Назначение | +| --- | --- | --- | +| `HYSTERIA_CHANNEL` | `stable` | `stable` — разрешить последнюю стабильную; `pinned` — офлайн‑сборка по `tools/build/hysteria-lock.env` | +| `HYSTERIA_VERSION_OVERRIDE` | пусто | Закрепить конкретную версию `vX.Y.Z` | +| `HYSTERIA_COMPAT_GATE` | `true` | Compatibility gate; для release‑сборок обязателен | +| `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в lock‑файл | + +Полный E2E с реальным клиентом Hysteria запускается отдельно: + +```bash +HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh +``` + Результат: ```text @@ -748,7 +848,7 @@ dist/hy2xs-install-.tar.gz ls -lh dist/hy2xs-install-*.tar.gz sha256sum dist/hy2xs-install-*.tar.gz -tar -tzf dist/hy2xs-install-0.2.2.tar.gz | grep -E \ +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)$' ``` @@ -762,6 +862,9 @@ tar -tzf dist/hy2xs-install-0.2.2.tar.gz | grep -E \ ├── orchestrator/ # install-only orchestrator на Bun + TypeScript ├── package/ # skeleton будущего install package ├── tools/build/ # production builder и packaging pipeline +├── tools/test/ # end-to-end проверки с реальным клиентом Hysteria +├── docs/ # спецификации baseline, тестов и эксплуатации +├── CHANGELOG.md ├── README.md └── LICENSE ``` diff --git a/apps/controller/config.go b/apps/controller/config.go index fd4ebad..2c1e86b 100644 --- a/apps/controller/config.go +++ b/apps/controller/config.go @@ -4,9 +4,6 @@ import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" - "hy2xs-admin/dao" - "hy2xs-admin/model/bo" "hy2xs-admin/model/constant" "hy2xs-admin/model/dto" "hy2xs-admin/model/entity" @@ -183,72 +180,24 @@ func UpdateHysteria2Config(c *gin.Context) { vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c) } +// ExportHysteria2Config отдаёт оператору фактический серверный конфиг. +// +// Экспорт работает от исходного YAML, а не от типизированной модели: поля, +// о которых HY2XS ещё не знает, обязаны пережить выгрузку. Секреты при этом +// вырезаются — файл покидает сервер. func ExportHysteria2Config(c *gin.Context) { - hysteria2ServerConfig, err := service.GetHysteria2Config() + sanitized, err := service.ExportHysteria2ConfigYaml() if err != nil { vo.Fail(err.Error(), c) return } - // Значения по умолчанию - config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.Hysteria2TrafficStatsSecret}) - if err != nil { - vo.Fail(err.Error(), c) - return - } - - var hUIWebPort string - var trafficStatsSecret string - for _, item := range config { - if *item.Key == constant.HUIWebPort { - hUIWebPort = *item.Value - } else if *item.Key == constant.Hysteria2TrafficStatsSecret { - trafficStatsSecret = *item.Value - } - } - - if hUIWebPort == "" || trafficStatsSecret == "" { - logrus.Errorf("hUIWebPort or trafficStatsSecret is nil") - vo.Fail(constant.SysError, c) - return - } - - authHttpUrl, err := service.GetAuthHttpUrl() - if err != nil { - vo.Fail(err.Error(), c) - return - } - - authType := "http" - authHttpInsecure := true - var auth bo.ServerConfigAuth - auth.Type = &authType - var http bo.ServerConfigAuthHTTP - http.URL = &authHttpUrl - http.Insecure = &authHttpInsecure - auth.HTTP = &http - hysteria2ServerConfig.Auth = &auth - if hysteria2ServerConfig.TrafficStats == nil { - hysteria2ServerConfig.TrafficStats = &bo.ServerConfigTrafficStats{} - } - hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret - fileName := fmt.Sprintf("Hysteria2Config-%s.yaml", time.Now().Format("20060102150405")) - filePath := filepath.Join(constant.ExportPathDir, fileName) - if err = util.ExportFile(filePath, hysteria2ServerConfig, 1); err != nil { - vo.Fail(err.Error(), c) - return - } - - if !util.Exists(filePath) { - vo.Fail("file not exist", c) - return - } c.Header("Content-Type", "application/octet-stream") c.Header("Content-Transfer-Encoding", "binary") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) - c.File(filePath) + c.Data(200, "application/octet-stream", sanitized) } func ImportHysteria2Config(c *gin.Context) { diff --git a/apps/frontend/src/api/config/types.ts b/apps/frontend/src/api/config/types.ts index d180cc9..22d7c5d 100644 --- a/apps/frontend/src/api/config/types.ts +++ b/apps/frontend/src/api/config/types.ts @@ -26,6 +26,10 @@ export interface Hysteria2ServerConfig { cert: string; key: string; sniGuard?: string; + clientCA?: string; + }; + ech?: { + keyPath?: string; }; acme?: { domains: string[]; @@ -51,9 +55,14 @@ export interface Hysteria2ServerConfig { }; obfs?: { type: string; - salamander: { + salamander?: { password: string; }; + gecko?: { + password: string; + minPacketSize?: number; + maxPacketSize?: number; + }; }; quic?: { initStreamReceiveWindow?: number; @@ -63,10 +72,16 @@ export interface Hysteria2ServerConfig { maxIdleTimeout?: string; maxIncomingStreams?: number; disablePathMTUDiscovery?: boolean; + disableStatelessReset?: boolean; }; bandwidth?: { up: string; down: string; + disableLossCompensation?: boolean; + }; + congestion?: { + type?: string; + bbrProfile?: string; }; ignoreClientBandwidth?: boolean; speedTest?: boolean; @@ -122,6 +137,7 @@ export interface Hysteria2ServerConfig { url: string; rewriteHost: boolean; insecure: boolean; + xForwarded?: boolean; }; string?: { content: string; @@ -132,6 +148,26 @@ export interface Hysteria2ServerConfig { listenHTTPS?: string; forceHTTPS?: boolean; }; + mimic?: { + enabled?: boolean; + interface?: string; + xdpMode?: string; + path?: string; + extraArgs?: string[]; + }; + realm?: { + stunServers?: string[]; + stunTimeout?: string; + punchTimeout?: string; + heartbeatInterval?: string; + insecure?: boolean; + ipMode?: string; + portMapping?: { + enabled?: boolean; + timeout?: string; + lifetime?: string; + }; + }; } export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = { @@ -144,9 +180,9 @@ export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = { acme: { domains: [], email: "", - ca: "zerossl", + ca: "letsencrypt", listenHost: "0.0.0.0", - dir: "my_acme_dir", + dir: "/var/lib/hysteria/acme", type: "", http: { altPort: 8888, @@ -155,7 +191,7 @@ export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = { altPort: 44333, }, dns: { - name: "gomommy", + name: "cloudflare", config: {}, }, disableHTTP: false, @@ -164,9 +200,11 @@ export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = { altTLSALPNPort: 443, }, obfs: { - type: "salamander", - salamander: { - password: "cry_me_a_r1ver", + type: "gecko", + gecko: { + password: "", + minPacketSize: 512, + maxPacketSize: 1200, }, }, quic: { @@ -177,10 +215,16 @@ export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = { maxIdleTimeout: "30s", maxIncomingStreams: 1024, disablePathMTUDiscovery: false, + disableStatelessReset: false, }, bandwidth: { - up: "1 gbps", - down: "1 gbps", + up: "50 mbps", + down: "50 mbps", + disableLossCompensation: false, + }, + congestion: { + type: "bbr", + bbrProfile: "standard", }, ignoreClientBandwidth: false, speedTest: false, @@ -236,6 +280,7 @@ export const defaultHysteria2ServerConfig: Hysteria2ServerConfig = { url: "", rewriteHost: true, insecure: false, + xForwarded: false, }, string: { content: "hello stupid world", diff --git a/apps/frontend/src/lang/package/en.ts b/apps/frontend/src/lang/package/en.ts index 195d944..480d8eb 100644 --- a/apps/frontend/src/lang/package/en.ts +++ b/apps/frontend/src/lang/package/en.ts @@ -128,6 +128,17 @@ export default { name: "Peer", remark: "Remark", secret: "Secret", + form: { + namePlaceholder: "e.g. ivan-laptop", + nameHint: + "Short peer identifier. Use latin letters, digits and hyphens — the name becomes part of the auto-generated secret.", + remarkPlaceholder: "e.g. Ivan's laptop, sales team", + remarkHint: "Optional operator note. It is never shown to the client.", + secretPlaceholder: "leave empty to generate automatically", + secretHint: + "Client connection password. Leave empty to generate one automatically. If set manually: 6 to 128 characters.", + quotaHint: "Traffic limit in bytes. Use -1 for unlimited.", + }, maxDevices: "Max devices", disabled: "Disabled", status: "Status", @@ -234,6 +245,7 @@ export default { obfs: "Obfuscation", quic: "QUIC parameters", bandwidth: "Bandwidth", + congestion: "Congestion control", speedTest: "Speed Test", udp: "UDP", resolver: "Resolver", @@ -282,10 +294,17 @@ export default { "Alternate TLS-ALPN challenge port. (Note: If you want to use anything other than 443, you must set up port forward/SNI proxy from 443 to that port, otherwise ACME will not be able to issue the certificate.)", }, obfs: { - type: "Type", + type: "Obfuscation type: gecko (HY2XS production default) or salamander (compatibility fallback).", salamander: { password: "Replace with a strong password of your choice.", }, + gecko: { + password: "Replace with a strong password of your choice.", + minPacketSize: + "Minimum QUIC handshake fragment size. HY2XS baseline: 512.", + maxPacketSize: + "Maximum QUIC handshake fragment size. HY2XS baseline: 1200, upstream limit: 2048.", + }, }, quic: { initStreamReceiveWindow: "The initial QUIC stream receive window size.", @@ -299,10 +318,18 @@ export default { maxIncomingStreams: "The maximum number of concurrent incoming streams.", disablePathMTUDiscovery: "Disable QUIC path MTU discovery.", + disableStatelessReset: + "Disable QUIC stateless reset. Kept off in the HY2XS baseline: stateless reset lets a client with a stale connection reconnect immediately after a server restart or device sleep.", }, bandwidth: { up: "Up", down: "Down", + disableLossCompensation: + "Disable loss compensation. Kept off in the HY2XS baseline, so compensation stays active.", + }, + congestion: { + type: "Fallback congestion controller: bbr or reno. Used when Brutal bandwidth is not negotiated by both sides.", + bbrProfile: "BBR profile: standard, conservative or aggressive.", }, ignoreClientBandwidth: "When enabled, makes the server to disregard any bandwidth hints set by clients", diff --git a/apps/frontend/src/lang/package/ru.ts b/apps/frontend/src/lang/package/ru.ts index f2aecbc..b66c325 100644 --- a/apps/frontend/src/lang/package/ru.ts +++ b/apps/frontend/src/lang/package/ru.ts @@ -124,6 +124,17 @@ export default { name: "Пир", remark: "Комментарий", secret: "Секрет", + form: { + namePlaceholder: "например, ivan-laptop", + nameHint: + "Короткий идентификатор пира. Используйте латиницу, цифры и дефис — имя попадает в автогенерируемый секрет.", + remarkPlaceholder: "например, Ноутбук Ивана, отдел продаж", + remarkHint: "Необязательная пометка для оператора. Клиент её не видит.", + secretPlaceholder: "оставьте пустым — сгенерируем автоматически", + secretHint: + "Пароль подключения клиента. Если оставить поле пустым, секрет будет сгенерирован автоматически. При ручном вводе: от 6 до 128 символов.", + quotaHint: "Лимит трафика в байтах. Укажите -1 для безлимита.", + }, maxDevices: "Лимит устройств", disabled: "Отключён", status: "Статус", @@ -230,6 +241,7 @@ export default { obfs: "Маскировка", quic: "Параметры QUIC", bandwidth: "Полоса", + congestion: "Congestion control", speedTest: "Тест скорости", udp: "UDP", resolver: "DNS", @@ -264,8 +276,15 @@ export default { altTLSALPNPort: "Альтернативный TLS-ALPN-порт", }, obfs: { - type: "Тип", + type: "Тип обфускации: gecko (production default HY2XS) или salamander (совместимость)", salamander: { password: "Сильный пароль Salamander" }, + gecko: { + password: "Сильный пароль Gecko", + minPacketSize: + "Минимальный размер фрагмента QUIC handshake. Baseline HY2XS: 512", + maxPacketSize: + "Максимальный размер фрагмента QUIC handshake. Baseline HY2XS: 1200, верхний предел upstream: 2048", + }, }, quic: { initStreamReceiveWindow: "Начальное окно приёма QUIC stream", @@ -275,8 +294,19 @@ export default { maxIdleTimeout: "Максимальный idle timeout", maxIncomingStreams: "Максимум входящих stream", disablePathMTUDiscovery: "Отключить QUIC path MTU discovery", + disableStatelessReset: + "Отключить QUIC stateless reset. В baseline HY2XS выключено: stateless reset ускоряет переподключение клиента после перезапуска сервера или сна устройства", + }, + bandwidth: { + up: "Вверх", + down: "Вниз", + disableLossCompensation: + "Отключить компенсацию потерь. В baseline HY2XS выключено, то есть компенсация работает", + }, + congestion: { + type: "Fallback congestion controller: bbr или reno. Применяется, когда Brutal bandwidth не согласован сторонами", + bbrProfile: "Профиль BBR: standard, conservative или aggressive", }, - bandwidth: { up: "Вверх", down: "Вниз" }, ignoreClientBandwidth: "Игнорировать bandwidth, заявленный клиентом", speedTest: "Встроенный сервер теста скорости", disableUDP: "Отключить UDP forwarding", diff --git a/apps/frontend/src/views/hysteria/list/index.vue b/apps/frontend/src/views/hysteria/list/index.vue index f892047..72638ee 100644 --- a/apps/frontend/src/views/hysteria/list/index.vue +++ b/apps/frontend/src/views/hysteria/list/index.vue @@ -361,21 +361,69 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (["dns-san", "disable", "strict"]); const aclTypes = ref(["file", "inline"]); const acmeCas = ref(["zerossl", "letsencrypt"]); const acmeTypes = ref(["http", "tls", "dns"]); +// Актуальный upstream-реестр ACME DNS-провайдеров. +// `namedotcom` удалён в Hysteria 2.11.0 вместе с переписанным ACME-стеком: +// конфигурация с ним больше не запускается. const dnsNames = ref([ "cloudflare", "duckdns", "gandi", "godaddy", - "namedotcom", + "namecheap", + "njalla", + "porkbun", "vultr", ]); -const obfsTypes = ref(["salamander"]); +const obfsTypes = ref(["gecko", "salamander"]); +const congestionTypes = ref(["bbr", "reno"]); +const bbrProfiles = ref(["standard", "conservative", "aggressive"]); const resolverTypes = ref(["tcp", "udp", "tls", "https"]); const masqueradeTypes = ref(["file", "proxy", "string"]); @@ -1043,6 +1169,7 @@ const state = reactive({ obfs: false, quic: false, bandwidth: false, + congestion: false, speedTest: false, udp: false, resolver: false, @@ -1067,6 +1194,7 @@ const { obfs, quic, bandwidth, + congestion, speedTest, udp, resolver, @@ -1120,6 +1248,7 @@ const setConfig = () => { state.obfs = !!data?.obfs; state.quic = !!data?.quic; state.bandwidth = !!data?.bandwidth; + state.congestion = !!data?.congestion; state.speedTest = !!data?.speedTest; state.udp = !!data?.disableUDP || !!data?.udpIdleTimeout; state.resolver = !!data?.resolver; @@ -1130,10 +1259,28 @@ const setConfig = () => { state.dataForm = deepCopy(defaultHysteria2ServerConfig); assignWith(state.dataForm, data); + dropInactiveObfsSubtype(state.dataForm, data); } }); }; +// Форма строится как merge дефолта и ответа API, поэтому неактивная ветка obfs +// осталась бы от дефолта и UI показал бы блок, которого нет в конфиге сервера. +const dropInactiveObfsSubtype = ( + form: Hysteria2ServerConfig, + data: Hysteria2ServerConfig +) => { + if (!form.obfs) { + return; + } + if (form.obfs.type !== "gecko" || !data?.obfs?.gecko) { + delete form.obfs.gecko; + } + if (form.obfs.type !== "salamander" || !data?.obfs?.salamander) { + delete form.obfs.salamander; + } +}; + const setHysteria2Monitor = async () => { const { data } = await dashboardSummaryApi(); state.hysteria2Monitor.version = data.hysteria.version; diff --git a/apps/frontend/src/views/peer/list/index.vue b/apps/frontend/src/views/peer/list/index.vue index b8956d2..6ab939b 100644 --- a/apps/frontend/src/views/peer/list/index.vue +++ b/apps/frontend/src/views/peer/list/index.vue @@ -65,9 +65,7 @@