Hysteria 2 каскад: различия между версиями

Материал из wolfram
Перейти к навигации Перейти к поиску
Новая страница: « = Hysteria 2: каскад РФ → EU — полная инструкция = Инструкция охватывает полный цикл: поднять **сервер Hysteria 2** на EU-VPS, настроить на нём **релей sing-box** на РФ-VPS, сгенерировать ссылки для клиентов и выполнить обслуживание серверов. Все команды выполняются **по одно...»
 
 
(не показаны 2 промежуточные версии этого же участника)
Строка 1: Строка 1:
= Hysteria2: установка каскадной цепочки Entry Node → Exit Node → WARP =


= Hysteria 2: каскад РФ → EU — полная инструкция =
== Что такое Hysteria2 и зачем он нужен ==
Инструкция охватывает полный цикл: поднять **сервер Hysteria 2** на EU-VPS, настроить на нём **релей sing-box** на РФ-VPS, сгенерировать ссылки для клиентов и выполнить обслуживание серверов.


Все команды выполняются **по одной** (или одним логическим блоком — запись файла), результат после каждой шага — проверяемый.
=== Проблема: ограничения традиционных прокси-протоколов ===


== Схема каскада ==
Современные системы глубокой инспекции пакетов (DPI — Deep Packet Inspection), применяемые интернет-провайдерами и регуляторами по всему миру, умеют анализировать не только адреса назначения, но и '''характер самого трафика'''. Даже зашифрованное соединение можно идентифицировать — по поведению, по паузам между пакетами, по размерам передаваемых блоков данных, по TLS-fingerprint.
<pre>
 
Клиент / роутер
Большинство прокси-протоколов работают поверх '''TCP'''. Это означает:
      │  Hysteria 2 (пароль «клиент→РФ») UDP :40000
* Все потери пакетов обрабатываются дважды — сначала на уровне транспорта, затем внутри туннеля (head-of-line blocking).
      ▼
* При нестабильном канале (мобильная сеть, спутник, VPN на слабом железе) скорость резко падает.
РФ-VPS  81.30.105.134  (sing-box)
* TCP-fingerprint легче распознаётся и блокируется.
  ├─ inbound: hysteria2  UDP :40000  ← от клиентов
 
  └─ outbound: hysteria2 → EU  UDP :40001  (пароль «РФ→EU»)
Hysteria2 решает эту проблему фундаментально иначе: он работает поверх '''QUIC''' — транспортного протокола на базе UDP, разработанного в Google и стандартизированного в RFC 9000.
      │
 
      ▼
=== Решение: QUIC/UDP с маскировкой под HTTPS ===
EU-VPS  148.253.213.5  (hysteria server)
 
  └─ UDP :40001  → выход в интернет
Hysteria2 строит зашифрованный туннель поверх UDP, используя QUIC:
</pre>
* '''Нет head-of-line blocking:''' потеря одного пакета не блокирует остальные потоки.
* '''Быстрое восстановление соединения:''' QUIC переподключается за 0-RTT (без дополнительного round-trip).
* '''Маскировка под HTTPS:''' снаружи трафик Hysteria2 выглядит как обычный QUIC/HTTP3 браузерный трафик — сервер «притворяется» обычным HTTPS-сайтом и возвращает реальный веб-контент при неавторизованных запросах.
* '''Congestion control:''' поддерживает несколько алгоритмов управления перегрузкой, включая BBR.
 
{| class="wikitable"
|-
! Протокол !! Транспорт !! Как выглядит для DPI !! Устойчивость к блокировке !! Скорость при потерях
|-
|| Обычный VPN (WireGuard, OpenVPN) || UDP / TCP || Характерный шаблон || Низкая — fingerprint легко детектируется || Высокая (WG) / Низкая (OpenVPN)
|-
|| Shadowsocks || TCP || Случайный зашифрованный поток || Средняя — выявляется по энтропии || Средняя
|-
|| VLESS + REALITY || TCP (TLS) || TLS с реальным сертификатом стороннего сайта || Высокая || Средняя
|-
|| NaïveProxy || TCP (HTTP/2 TLS) || Неотличим от Chrome HTTPS || Очень высокая || Средняя (TCP Head-of-Line)
|-
|| '''Hysteria2''' || '''UDP (QUIC/TLS)''' || '''QUIC/HTTP3, маскировка под HTTPS''' || '''Очень высокая''' || '''Высокая — устойчив к потерям'''
|}
 
=== Как работает Hysteria2 технически ===
 
Hysteria2 использует протокол QUIC в качестве транспорта:
 
# Клиент устанавливает QUIC-соединение с сервером — это TLS 1.3 поверх UDP.
# Внутри QUIC-соединения открываются независимые потоки (streams) для каждого проксируемого соединения.
# Сервер обрабатывает входящий трафик: авторизует клиента (по паролю или через внешний скрипт), затем перенаправляет трафик через указанный outbound.
# Если к серверу обращаются без правильного пароля — сервер ведёт себя как обычный HTTPS-сайт ('''masquerade''') и возвращает реальный контент с заданного URL.
 
Особенности аутентификации в данной схеме:
* '''EU сервер:''' использует одиночный пароль — только для входящих соединений от RU клиента (не клиентских устройств).
* '''RU сервер:''' использует '''command-аутентификацию''' — при каждом подключении вызывается внешний bash-скрипт, который проверяет пароль по файлу <code>users.txt</code>. Это позволяет добавлять и удалять пользователей '''без перезапуска сервера'''.
 
=== Каскадная цепочка: зачем два сервера ===
 
В данном решении используется '''каскад из двух серверов''': Entry Node (ближайший к клиентам сервер-вход) и Exit Node (сервер-выход в интернет). Это архитектурное решение принято намеренно по нескольким причинам:
 
* '''Снижение риска для клиента:''' клиентские устройства подключаются только к ближайшему серверу. Соединение с соседним узлом менее заметно, чем прямое соединение с зарубежным сервером.
* '''Раздельная аутентификация:''' пароль «клиент → Entry Node» знают только конечные пользователи. Пароль «Entry Node → Exit Node» знают только серверы. Компрометация клиентского пароля не раскрывает пароль соединения между серверами.
* '''Анонимизация выхода:''' Exit Node передаёт трафик через Cloudflare WARP. Конечные сайты видят IP Cloudflare, а не IP ваших серверов.
* '''Параллельные независимые каналы:''' Hysteria2 работает рядом с NaïveProxy и 3x-ui на тех же серверах, не конфликтуя с ними. Падение одного канала не блокирует другие.
 
----
 
== Схема конкретного решения ==


; Почему два пароля
=== Компоненты инфраструктуры ===
: '''Пароль «клиент → РФ»''' — знают только ваши устройства и sing-box РФ.
: '''Пароль «РФ → EU»''' — знают только sing-box РФ и hysteria EU. На клиентах он '''не''' нужен.


== Параметры развёртывания ==
{| class="wikitable"
{| class="wikitable"
!Параметр
!Значение
!Примечание
|-
|-
|ОС EU
! Компонент !! Роль !! Адрес / Домен !! ОС
|Ubuntu 24.04 LTS
|-
|apt / systemd
|| Клиентские устройства || Конечные пользователи || — || iOS, Android, Windows, macOS, Linux
|-
|| OpenWrt роутер || Прозрачный обход для всей домашней сети || — || OpenWrt 24.10 (podkop нативно поддерживает hy2 URI)
|-
|| RU сервер ('''Entry Node''') || Точка входа клиентов || <code>YOUR_RU_SERVER_IP</code> / <code>your-hy2-ru-domain.example.com</code> || Ubuntu 24.04
|-
|| EU сервер ('''Exit Node''') || Точка выхода в интернет || <code>YOUR_EU_SERVER_IP</code> / <code>your-hy2-eu-domain.example.com</code> || Ubuntu 24.04
|-
|-
|IP EU (публичный)
|| Cloudflare WARP || Финальный выход в интернет || IP Cloudflare || —
|<code>148.253.213.5</code>
|}
|Изменить на свой
 
=== Полная схема прохождения трафика ===
 
[[File:hysteria2-chain-traffic-flow.png|center|768x768px|Схема трафика Hysteria2: Клиенты → RU Entry Node → EU Exit Node → WARP → Интернет]]
 
=== Протоколы на каждом участке цепочки ===
 
{| class="wikitable"
|-
|-
|UDP-порт EU
! Участок !! Протокол !! Шифрование !! Что видит наблюдатель
|<code>40001</code>
|Вход для РФ-релея
|-
|-
|Бинарь EU
|| Устройство → Роутер || Обычный TCP/UDP || Нет (локальная сеть) || Локальный трафик
|<code>hysteria v2.8.1</code>
|<code>/usr/local/bin/hysteria</code>
|-
|-
|Конфиг EU
|| Роутер / ПК → RU сервер || QUIC / UDP (Hysteria2) || TLS 1.3 (Let's Encrypt) || HTTPS/HTTP3 трафик к обычному сайту
|<code>/etc/hysteria/server-eu.yaml</code>
|Права 0640
|-
|-
|TLS EU
|| RU клиент → EU сервер || QUIC / UDP (Hysteria2) || TLS 1.3 (Let's Encrypt) || HTTPS/HTTP3 трафик к обычному сайту
|<code>/etc/hysteria/tls-eu/eu.crt</code> + <code>eu.key</code>
|Самоподпись на IPv4
|-
|-
|Сервис EU
|| EU сервер → 3x-ui inbound || SOCKS5 (localhost) || — (только на EU сервере) || Только локально
|<code>hysteria-eu-hy2test.service</code>
|systemd
|-
|-
|ОС РФ
|| EU 3x-ui → WARP || WireGuard (gVisor TUN) || WireGuard || Зашифрованный WireGuard к Cloudflare
|Debian 13 (trixie)
|apt / systemd
|-
|-
|IP РФ (публичный)
|| WARP → Интернет || Обычный TCP/UDP || — || IP Cloudflare
|<code>81.30.105.134</code>
|}
|Изменить на свой
 
=== Дополнительные каналы (параллельно Hysteria2) ===
 
Hysteria2 работает '''независимо''' от NaïveProxy и 3x-ui. Все три канала активны одновременно:
 
{| class="wikitable"
|-
|-
|UDP-порт РФ (вход клиентов)
! Канал !! Протокол !! Маршрут !! Статус
|<code>40000</code>
|Не пересекаться с Xray
|-
|-
|Бинарь РФ
|| 3x-ui каскад (основной) || VLESS / REALITY || Клиенты → RU 3x-ui → EU 3x-ui → Интернет || ✅ Активен
|<code>sing-box v1.13.8</code>
|<code>/usr/local/bin/sing-box</code>
|-
|-
|Конфиг РФ
|| NaïveProxy каскад || HTTP/2 naïve || Роутер/ПК → RU Caddy → EU Caddy → WARP || ✅ Активен
|<code>/etc/sing-box/config.json</code>
|Права 0600
|-
|-
|TLS РФ
|| '''Hysteria2 каскад (этот гайд)''' || '''QUIC/UDP hy2''' || '''Клиенты → RU hy2 → EU hy2 → WARP''' || '''✅ Активен'''
|<code>/etc/sing-box/tls-ru/ru.crt</code> + <code>ru.key</code>
|Самоподпись на IPv4
|-
|-
|Сервис РФ
|| MTProto (Telegram) || MTProto || Клиенты Telegram → EU :8443, :2443 || ✅ Активен
|<code>sing-box-hy2relay-test.service</code>
|systemd
|}
|}
----
----


= Часть 1. Сервер EU (Ubuntu 24.04) =
== Предварительные требования ==
 
=== Инфраструктура ===
 
{| class="wikitable"
|-
! Компонент !! Требования
|-
|| EU сервер || Ubuntu 24.04, публичный IP, домен с A-записью → IP сервера
|-
|| RU сервер || Ubuntu 24.04, публичный IP, домен с A-записью → IP сервера
|-
|| 3x-ui на EU сервере || Уже установлен и работает; настроен outbound WARP
|-
|| Docker || Установлен на обоих серверах
|-
|| Порт 2053 UDP || Должен быть свободен на обоих серверах
|-
|| TLS-сертификат || Получается через Caddy (caddy-naive), либо через certbot — описано в разделе ниже
|}
 
=== DNS-записи (создать заранее, до установки) ===


== 1.1 Диагностика перед установкой ==
Let's Encrypt проверяет доступность домена по HTTP (порт 80) перед выдачей сертификата. DNS-записи должны быть созданы и распространены (propagated) до первого запуска Caddy.
Выполнять на '''EU-VPS''' под root.


=== 1.1.1 Дата и время ===
{| class="wikitable"
<syntaxhighlight lang="bash">
|-
date -u
! Запись !! Тип !! Значение !! Примечание
timedatectl status
|-
</syntaxhighlight>'''Ожидаемо:''' <code>System clock synchronized: yes</code>, <code>NTP service: active</code>.
|| <code>your-hy2-ru-domain.example.com</code> || A || IP RU сервера || Используется Hysteria2 сервером на RU
|-
|| <code>your-hy2-eu-domain.example.com</code> || A || IP EU сервера || Используется Hysteria2 сервером на EU
|}
 
'''Примечание:''' если NaïveProxy (Caddy) уже настроен на тех же серверах с теми же доменами, отдельные DNS-записи для Hysteria2 не нужны — используется тот же домен и тот же сертификат.
 
=== Используемые заглушки ===
 
В данном руководстве применяются следующие заглушки. Замени их реальными значениями перед использованием:
 
{| class="wikitable"
|-
! Заглушка !! Описание
|-
|| <code>YOUR_RU_SERVER_IP</code> || Публичный IP RU сервера
|-
|| <code>YOUR_EU_SERVER_IP</code> || Публичный IP EU сервера
|-
|| <code>your-hy2-ru-domain.example.com</code> || Домен для RU сервера (для TLS-сертификата)
|-
|| <code>your-hy2-eu-domain.example.com</code> || Домен для EU сервера (для TLS-сертификата)
|-
|| <code>YOUR_EMAIL@example.com</code> || Email для Let's Encrypt (уведомления об истечении сертификата)
|-
|| <code>EU_HY2_PASSWORD</code> || Пароль соединения RU клиент → EU сервер (генерируется один раз)
|}


=== 1.1.2 Публичный IP и интерфейсы ===
'''Для генерации пароля EU_HY2_PASSWORD используй:'''
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
curl -4sS --max-time 10 https://api.ipify.org; echo
openssl rand -base64 18
ip -br a
</syntaxhighlight>
</syntaxhighlight>
Сохрани результат — он понадобится в конфигах обоих серверов.
----
== Часть 1: EU сервер (Exit Node) ==
Цель: развернуть Hysteria2 сервер в Docker, который принимает соединения от RU клиента и перенаправляет трафик через 3x-ui → Cloudflare WARP.
'''Все команды в этом разделе выполняются на EU сервере под root.'''
=== 1.1 Установка Docker ===
Если Docker уже установлен (проверить: <code>docker --version</code>) — пропусти этот шаг.


=== 1.1.3 Занятые порты (не пересечься с Xray/Docker) ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
ss -tulpen | head -n 50
# Обновить пакеты
</syntaxhighlight>
apt-get update && apt-get upgrade -y
 
# Установить зависимости
apt-get install -y ca-certificates curl gnupg lsb-release


=== 1.1.4 Проверить, что UDP-порт 40001 свободен ===
# Добавить GPG-ключ Docker
<syntaxhighlight lang="bash">
install -m 0755 -d /etc/apt/keyrings
ss -ulpen | grep ':40001' && echo "ЗАНЯТ" || echo "OK: свободен"
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
</syntaxhighlight>
  -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc


== 1.2 Установка Hysteria ==
# Добавить репозиторий Docker
echo "deb [arch=$(dpkg --print-architecture) \
  signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  > /etc/apt/sources.list.d/docker.list


=== 1.2.1 Обновить пакеты ===
# Установить Docker Engine
<syntaxhighlight lang="bash">
apt-get update
apt-get update
apt-get install -y ca-certificates curl openssl
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
 
# Включить автозапуск
systemctl enable docker
systemctl start docker
 
# Проверить
docker --version
docker compose version
</syntaxhighlight>
</syntaxhighlight>


=== 1.2.2 Создать каталог конфигурации ===
Ожидаемый вывод:
<pre>
Docker version 27.x.x, build ...
Docker Compose version v2.x.x
</pre>
 
=== 1.2 Настройка 3x-ui: создание inbound hy2-out → WARP ===
 
Hysteria2 Exit Node направляет трафик через 3x-ui SOCKS5 inbound, который в свою очередь маршрутизирует его в Cloudflare WARP. Этот шаг выполняется в веб-интерфейсе 3x-ui '''до''' запуска Hysteria2.
 
'''Условие:''' в 3x-ui должен быть настроен outbound WARP (WireGuard/gVisor).
 
==== 1.2.1 Создать новый inbound в 3x-ui ====
 
В веб-интерфейсе 3x-ui (EU сервер) перейди в '''Inbounds → Add Inbound''' и заполни следующие поля:
 
{| class="wikitable"
|-
! Параметр !! Значение !! Пояснение
|-
|| Примечание (Remark) || <code>hy2-out</code> || Произвольное имя для идентификации в правилах маршрутизации
|-
|| Протокол || <code>mixed</code> || Поддерживает SOCKS5 и HTTP одновременно
|-
|| Listen IP || <code>127.0.0.1</code> || Только localhost — недоступен извне
|-
|| Порт || <code>24364</code> || Должен быть свободен; проверить: <code>ss -tlnp | grep 24364</code>
|-
|| Аутентификация || Выключена || Hysteria2 сам отвечает за авторизацию клиентов
|-
|| UDP || Включён || Требуется для DNS-запросов через туннель
|}
 
Нажми '''Save''' — inbound немедленно активируется.
 
==== 1.2.2 Создать правило маршрутизации ====
 
В 3x-ui перейди в '''Routing → Add Rule''' (или Settings → Routing Rules):
 
{| class="wikitable"
|-
! Параметр !! Значение
|-
|| Inbound Tag || <code>hy2-out</code> (тег созданного выше inbound)
|-
|| Outbound Tag || <code>warp</code> (или название твоего WARP outbound в 3x-ui)
|}
 
Сохрани и перезапусти Xray внутри 3x-ui.
 
==== 1.2.3 Проверить, что порт 24364 слушает ====
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
install -d -m 0755 /etc/hysteria
ss -tlnp | grep 24364
</syntaxhighlight>
</syntaxhighlight>


=== 1.2.3 Задать версию и URL ===
Ожидаемый вывод:
<pre>
LISTEN  0  128  127.0.0.1:24364  0.0.0.0:*  users:(("xray",...))
</pre>
 
=== 1.3 TLS-сертификат для Hysteria2 ===
 
Hysteria2 требует действующий TLS-сертификат (самоподписанный не рекомендуется — клиентам потребуется флаг <code>insecure: true</code>). В нашей схеме используется '''Let's Encrypt сертификат''', получаемый и автоматически обновляемый через Caddy (caddy-naive стек).
 
Caddy хранит сертификаты в именованном Docker volume <code>caddy-naive_caddy_data</code>, и Hysteria2 подключает их через bind mount (read-only) — никаких лишних сервисов, никакого дублирования.
 
==== Вариант А: Caddy (caddy-naive) уже установлен — рекомендуется ====
 
Если NaïveProxy (caddy-naive) уже настроен и работает, сертификат уже получен. Проверь:
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
export HY2_TAG="app/v2.8.1"
# Найти путь к сертификату в Docker volume
export HY2_BIN_URL="https://github.com/apernet/hysteria/releases/download/${HY2_TAG}/hysteria-linux-amd64"
DOMAIN="your-hy2-eu-domain.example.com"
echo "${HY2_BIN_URL}"
CERT_DIR="/var/lib/docker/volumes/caddy-naive_caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}"
 
ls -la "${CERT_DIR}/"
</syntaxhighlight>
</syntaxhighlight>


=== 1.2.4 Скачать бинарь (с повторами при 504) ===
Ожидаемый вывод — два файла:
<syntaxhighlight lang="bash">
<pre>
curl -4fL --connect-timeout 20 --max-time 300 --retry 8 --retry-delay 2 --retry-all-errors \
your-hy2-eu-domain.example.com.crt
  -o /tmp/hysteria "${HY2_BIN_URL}"
your-hy2-eu-domain.example.com.key
</syntaxhighlight>
</pre>
 
Если файлы есть — переходи к разделу '''1.4'''. Если volume или папка не существуют — сначала установи caddy-naive (NaïveProxy) или воспользуйся Вариантом Б.


=== 1.2.5 Проверить размер (~15–17 МБ) ===
==== Вариант Б: получить сертификат через certbot (если Caddy не установлен) ====
<syntaxhighlight lang="bash">
ls -la /tmp/hysteria
</syntaxhighlight>


=== 1.2.6 Установить ===
Этот вариант используется, если caddy-naive '''не установлен''' и на серверах нет другого HTTP-сервера на портах 80/443.
<syntaxhighlight lang="bash">
install -m 0755 /tmp/hysteria /usr/local/bin/hysteria
rm -f /tmp/hysteria
</syntaxhighlight>


=== 1.2.7 Версия ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
/usr/local/bin/hysteria version
# Установить certbot
</syntaxhighlight>'''Ожидаемо:''' строка <code>Version: v2.8.1</code>.
apt-get install -y certbot


== 1.3 TLS (самоподписанный сертификат на IPv4) ==
# Получить сертификат (standalone — Certbot запустит временный HTTP-сервер на :80)
Используется, когда нет домена с DNS-записью. Клиент подключается с <code>insecure: true</code> или по pin SHA-256.
certbot certonly --standalone \
  -d your-hy2-eu-domain.example.com \
  --email YOUR_EMAIL@example.com \
  --agree-tos --non-interactive


=== 1.3.1 Переменные ===
# Проверить
<syntaxhighlight lang="bash">
ls /etc/letsencrypt/live/your-hy2-eu-domain.example.com/
export HY2_IP="148.253.213.5"          # ← ваш публичный IP EU
export HY2_TLS_DIR="/etc/hysteria/tls-eu"
install -d -m 0755 "${HY2_TLS_DIR}"
</syntaxhighlight>
</syntaxhighlight>


=== 1.3.2 Сгенерировать сертификат EC P-256 с SAN=IP ===
Сертификаты будут по пути <code>/etc/letsencrypt/live/your-hy2-eu-domain.example.com/</code>. В этом случае в <code>docker-compose.yml</code> (раздел 1.6) замени volume на:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="yaml">
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
- /etc/letsencrypt/live/your-hy2-eu-domain.example.com:/etc/hysteria/certs:ro
  -days 3650 -nodes \
- /etc/letsencrypt/archive/your-hy2-eu-domain.example.com:/etc/letsencrypt/archive/your-hy2-eu-domain.example.com:ro
  -keyout "${HY2_TLS_DIR}/eu.key" \
  -out  "${HY2_TLS_DIR}/eu.crt" \
  -subj "/CN=${HY2_IP}" \
  -addext "subjectAltName=IP:${HY2_IP}"
</syntaxhighlight>
</syntaxhighlight>


=== 1.3.3 Права ===
И в <code>server.yaml</code> пути к файлам:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="yaml">
chmod 0640 "${HY2_TLS_DIR}/eu.key"
tls:
chmod 0644 "${HY2_TLS_DIR}/eu.crt"
  cert: /etc/hysteria/certs/fullchain.pem
  key: /etc/hysteria/certs/privkey.pem
</syntaxhighlight>
</syntaxhighlight>


=== 1.3.4 Проверить CN и SAN ===
=== 1.4 Создание структуры проекта ===
<syntaxhighlight lang="bash">
openssl x509 -in "${HY2_TLS_DIR}/eu.crt" -noout -subject -dates -ext subjectAltName
</syntaxhighlight>'''Ожидаемо:''' <code>CN = 148.253.213.5</code>, <code>IP Address:148.253.213.5</code>.


=== 1.3.5 (Опционально) Получить fingerprint SHA-256 для pin на клиентах ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
openssl x509 -in "${HY2_TLS_DIR}/eu.crt" -noout -fingerprint -sha256
mkdir -p /opt/hysteria-eu
</syntaxhighlight>Сохраните вывод — пригодится для <code>pinSHA256</code> в URI клиентов.
cd /opt/hysteria-eu
</syntaxhighlight>


== 1.4 Конфиг сервера и systemd ==
Итоговая структура:
<pre>
/opt/hysteria-eu/
├── docker-compose.yml
└── server.yaml
</pre>


=== 1.4.1 Задать порт и путь к TLS ===
=== 1.5 Конфигурация сервера: server.yaml ===
<syntaxhighlight lang="bash">
export HY2_UDP_PORT=40001
export HY2_TLS_DIR="/etc/hysteria/tls-eu"
</syntaxhighlight>


=== 1.4.2 Сгенерировать пароль линка РФ→EU (сохраните его в менеджер паролей) ===
Создай файл конфигурации. Замени <code>EU_HY2_PASSWORD</code> сгенерированным паролем:
<syntaxhighlight lang="bash">
export HY2_RU_TO_EU_PASSWORD="$(openssl rand -base64 32 | tr -d '\n')"
echo "${HY2_RU_TO_EU_PASSWORD}"
</syntaxhighlight>'''Важно:''' скопируйте значение. Оно понадобится при настройке РФ-сервера.


=== 1.4.3 Записать /etc/hysteria/server-eu.yaml ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
umask 077
cat > /opt/hysteria-eu/server.yaml << 'EOF'
tee /etc/hysteria/server-eu.yaml >/dev/null <<EOF
listen: :2053
listen: :${HY2_UDP_PORT}


tls:
tls:
   cert: ${HY2_TLS_DIR}/eu.crt
   cert: /etc/hysteria/certs/your-hy2-eu-domain.example.com.crt
   key: ${HY2_TLS_DIR}/eu.key
   key: /etc/hysteria/certs/your-hy2-eu-domain.example.com.key


auth:
auth:
   type: password
   type: password
   password: '${HY2_RU_TO_EU_PASSWORD}'
   password: EU_HY2_PASSWORD
 
ignoreClientBandwidth: true


masquerade:
masquerade:
Строка 225: Строка 385:
     url: https://news.ycombinator.com/
     url: https://news.ycombinator.com/
     rewriteHost: true
     rewriteHost: true
outbounds:
  - name: warp
    type: socks5
    socks5:
      addr: 127.0.0.1:24364
acl:
  inline:
    - "warp(all)"
EOF
EOF
chmod 0640 /etc/hysteria/server-eu.yaml
</syntaxhighlight>
</syntaxhighlight>


=== 1.4.4 Создать systemd-юнит ===
Описание ключевых параметров:
<syntaxhighlight lang="bash">
tee /etc/systemd/system/hysteria-eu-hy2test.service >/dev/null <<'EOF'
[Unit]
Description=Hysteria2 EU test (UDP)
After=network-online.target
Wants=network-online.target


[Service]
{| class="wikitable"
Type=simple
|-
ExecStart=/usr/local/bin/hysteria server -c /etc/hysteria/server-eu.yaml
! Параметр !! Значение !! Пояснение
Restart=on-failure
|-
RestartSec=2
|| <code>listen: :2053</code> || UDP порт 2053 || Hysteria2 слушает на всех интерфейсах
LimitNOFILE=1048576
|-
|| <code>tls.cert / tls.key</code> || Пути к сертификату || Берутся из caddy-naive Docker volume (bind mount)
|-
|| <code>auth.type: password</code> || Одиночный пароль || Для соединения RU клиент → EU сервер (клиентские устройства этот пароль не знают)
|-
|| <code>ignoreClientBandwidth: true</code> || Игнорировать лимит клиента || Серверная сторона управляет congestion control самостоятельно
|-
|| <code>masquerade.type: proxy</code> || Проксировать реальный сайт || Неавторизованные запросы получают реальный контент ycombinator.com — сервер выглядит как обычный HTTPS-сайт
|-
|| <code>outbounds.warp</code> || SOCKS5 на 127.0.0.1:24364 || Исходящий трафик → 3x-ui inbound "hy2-out" → WARP
|-
|| <code>acl: warp(all)</code> || Направить весь трафик в warp || Синтаксис: имя outbound в скобках, адрес/маска после
|}


[Install]
=== 1.6 Конфигурация Docker Compose: docker-compose.yml ===
WantedBy=multi-user.target
EOF
</syntaxhighlight>


=== 1.4.5 Включить и запустить ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl daemon-reload
cat > /opt/hysteria-eu/docker-compose.yml << 'EOF'
systemctl enable hysteria-eu-hy2test.service
services:
systemctl start  hysteria-eu-hy2test.service
  hysteria-eu:
    image: tobyxdd/hysteria:latest
    container_name: hysteria-eu
    restart: unless-stopped
    network_mode: host
    command: server --config /etc/hysteria/server.yaml
    volumes:
      - ./server.yaml:/etc/hysteria/server.yaml:ro
      - /var/lib/docker/volumes/caddy-naive_caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/your-hy2-eu-domain.example.com:/etc/hysteria/certs:ro
EOF
</syntaxhighlight>
</syntaxhighlight>


=== 1.4.6 Проверить статус ===
Важные моменты:
<syntaxhighlight lang="bash">
* <code>network_mode: host</code> — обязателен, чтобы контейнер видел <code>127.0.0.1:24364</code> (3x-ui inbound).
systemctl --no-pager -l status hysteria-eu-hy2test.service
* <code>:ro</code> — сертификаты подключены только для чтения, Hysteria2 их не изменяет.
</syntaxhighlight>'''Ожидаемо:''' <code>Active: active (running)</code>.
* Путь к сертификату — bind mount из Docker volume caddy-naive. Если используется certbot (Вариант Б) — замени путь согласно инструкции раздела 1.3.


=== 1.4.7 Убедиться, что UDP-порт слушается ===
=== 1.7 Первый запуск ===
<syntaxhighlight lang="bash">
ss -ulpen | grep ':40001' || echo "НЕ СЛУШАЕТ"
</syntaxhighlight>'''Ожидаемо:''' процесс <code>hysteria</code>, <code>UNCONN</code>, порт <code>40001</code>.


=== 1.4.8 Логи ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
journalctl -u hysteria-eu-hy2test -n 40 --no-pager
cd /opt/hysteria-eu
</syntaxhighlight>'''Ожидаемо:''' строка <code>server up and running {"listen": ":40001"}</code>.


== 1.5 Локальный self-test (клиент на том же EU-VPS) ==
# Скачать образ и запустить
docker compose pull
docker compose up -d


=== 1.5.1 Прочитать пароль из конфига в переменную ===
# Подождать 3 секунды и проверить логи
<syntaxhighlight lang="bash">
sleep 3 && docker compose logs
PASSWORD="$(python3 - <<'PY'
import re
from pathlib import Path
text = Path("/etc/hysteria/server-eu.yaml").read_text(encoding="utf-8")
m = re.search(r"(?m)^\s*password:\s*'([^']*)'\s*$", text)
if not m:
    raise SystemExit("Не нашёл password в server-eu.yaml")
print(m.group(1))
PY
)"
echo "PASSWORD_len=${#PASSWORD}"
</syntaxhighlight>
</syntaxhighlight>


=== 1.5.2 Временный клиентский конфиг ===
Ожидаемый вывод в логах:
<pre>
INFO  server mode
INFO  server up and running  {"listen": ":2053"}
</pre>
 
Если в логах ошибка — см. раздел «Диагностика проблем» (Часть 5).
 
=== 1.8 Проверка: порт и соединение ===
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
cat > /tmp/hy2-eu-selftest.yaml <<EOF
# Проверить, что Hysteria2 слушает на UDP :2053
server: 127.0.0.1:40001
ss -ulnp | grep ':2053'
auth: ${PASSWORD}
tls:
  insecure: true
socks5:
  listen: 127.0.0.1:19800
EOF
</syntaxhighlight>
</syntaxhighlight>


=== 1.5.3 Запустить клиент в фоне ===
Ожидаемый вывод:
<pre>
UNCONN  0  0  0.0.0.0:2053  0.0.0.0:*  users:(("hysteria",...))
</pre>
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
hysteria client -c /tmp/hy2-eu-selftest.yaml >/tmp/hy2-eu-selftest.log 2>&1 &
# Статус контейнера
sleep 1
docker compose ps
</syntaxhighlight>
</syntaxhighlight>


=== 1.5.4 Проверить IP через SOCKS5 ===
<pre>
<syntaxhighlight lang="bash">
NAME          IMAGE                    STATUS
curl -4fsS --max-time 15 --socks5-hostname 127.0.0.1:19800 https://api.ipify.org; echo
hysteria-eu  tobyxdd/hysteria:latest  Up X minutes
</syntaxhighlight>'''Ожидаемо:''' <code>148.253.213.5</code> (ваш EU-IP).
</pre>


=== 1.5.5 Остановить и убрать ===
<syntaxhighlight lang="bash">
pkill -f "/tmp/hy2-eu-selftest.yaml" || true
rm -f /tmp/hy2-eu-selftest.yaml /tmp/hy2-eu-selftest.log
</syntaxhighlight>
----
----


= Часть 2. Сервер РФ (Debian 13) — релей sing-box =
== Часть 2: RU сервер (Entry Node) ==
 
Цель: развернуть два Docker-контейнера:
* <code>hysteria-ru-server</code> — принимает соединения от клиентов, аутентифицирует по <code>users.txt</code>.
* <code>hysteria-ru-client</code> — подключается к EU серверу, слушает SOCKS5 на <code>127.0.0.1:10810</code>.
 
Между ними: hysteria-ru-server направляет трафик через SOCKS5 → hysteria-ru-client → EU.


== 2.1 Диагностика перед установкой ==
'''Все команды в этом разделе выполняются на RU сервере под root.'''
Выполнять на '''РФ-VPS''' под root.
 
=== 2.1 Установка Docker ===
 
Аналогично EU серверу (раздел 1.1). Если Docker уже установлен — пропусти.


=== 2.1.1 Публичный IP и порты ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
curl -4sS --max-time 10 https://api.ipify.org; echo
apt-get update && apt-get upgrade -y
ss -tulpen | head -n 50
apt-get install -y ca-certificates curl gnupg lsb-release
</syntaxhighlight>


=== 2.1.2 Проверить, что UDP-порт 40000 свободен ===
install -m 0755 -d /etc/apt/keyrings
<syntaxhighlight lang="bash">
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
ss -ulpen | grep ':40000' && echo "ЗАНЯТ" || echo "OK: свободен"
  -o /etc/apt/keyrings/docker.asc
</syntaxhighlight>
chmod a+r /etc/apt/keyrings/docker.asc


== 2.2 Установка sing-box ==
echo "deb [arch=$(dpkg --print-architecture) \
  signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  > /etc/apt/sources.list.d/docker.list


=== 2.2.1 Пакеты ===
<syntaxhighlight lang="bash">
apt-get update
apt-get update
apt-get install -y ca-certificates curl tar openssl
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
systemctl enable docker && systemctl start docker
 
docker --version && docker compose version
</syntaxhighlight>
</syntaxhighlight>


=== 2.2.2 Каталоги ===
=== 2.2 TLS-сертификат для RU сервера ===
 
Аналогично EU серверу. На RU сервере также используется сертификат от Caddy (caddy-naive), если NaïveProxy настроен.
 
==== Вариант А: через Caddy (caddy-naive) — рекомендуется ====
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
install -d -m 0755 /etc/sing-box
DOMAIN="your-hy2-ru-domain.example.com"
install -d -m 0755 /etc/sing-box/tls-ru
CERT_DIR="/var/lib/docker/volumes/caddy-naive_caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}"
 
ls -la "${CERT_DIR}/"
</syntaxhighlight>
</syntaxhighlight>


=== 2.2.3 Задать версию и скачать архив ===
Если файлы <code>.crt</code> и <code>.key</code> присутствуют — всё готово, переходи к разделу 2.3.
<syntaxhighlight lang="bash">
export SB_VER="1.13.8"
export SB_TAG="v${SB_VER}"
export SB_TARBALL="sing-box-${SB_VER}-linux-amd64-glibc.tar.gz"
export SB_URL="https://github.com/SagerNet/sing-box/releases/download/${SB_TAG}/${SB_TARBALL}"
curl -4fL --connect-timeout 20 --max-time 300 --retry 8 --retry-delay 2 --retry-all-errors \
  -o "/tmp/${SB_TARBALL}" "${SB_URL}"
</syntaxhighlight>'''Важно:''' каталог внутри архива называется <code>sing-box-X.Y.Z-linux-amd64-'''glibc'''</code>, не просто <code>…-linux-amd64</code>.


=== 2.2.4 Распаковать и установить ===
==== Вариант Б: через certbot (если Caddy не установлен) ====
<syntaxhighlight lang="bash">
tar -C /tmp -xzf "/tmp/${SB_TARBALL}"
install -m 0755 "/tmp/sing-box-${SB_VER}-linux-amd64-glibc/sing-box" /usr/local/bin/sing-box
rm -rf "/tmp/sing-box-${SB_VER}-linux-amd64-glibc" "/tmp/${SB_TARBALL}"
</syntaxhighlight>


=== 2.2.5 Версия ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
sing-box version
apt-get install -y certbot
</syntaxhighlight>'''Ожидаемо:''' <code>sing-box version 1.13.8</code>.


== 2.3 TLS для входа Hy2 (самоподпись на IPv4 РФ) ==
certbot certonly --standalone \
  -d your-hy2-ru-domain.example.com \
  --email YOUR_EMAIL@example.com \
  --agree-tos --non-interactive


=== 2.3.1 Переменная IP ===
ls /etc/letsencrypt/live/your-hy2-ru-domain.example.com/
<syntaxhighlight lang="bash">
export HY2_RU_IP="81.30.105.134"      # ← ваш публичный IP РФ
</syntaxhighlight>
</syntaxhighlight>


=== 2.3.2 Сгенерировать сертификат ===
=== 2.3 Создание структуры проекта ===
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
mkdir -p /opt/hysteria-ru/users
  -days 3650 -nodes \
cd /opt/hysteria-ru
  -keyout /etc/sing-box/tls-ru/ru.key \
  -out  /etc/sing-box/tls-ru/ru.crt \
  -subj "/CN=${HY2_RU_IP}" \
  -addext "subjectAltName=IP:${HY2_RU_IP}"
chmod 0640 /etc/sing-box/tls-ru/ru.key
chmod 0644 /etc/sing-box/tls-ru/ru.crt
</syntaxhighlight>
</syntaxhighlight>


=== 2.3.3 Проверить ===
Итоговая структура:
<pre>
/opt/hysteria-ru/
├── docker-compose.yml
├── server.yaml
├── client.yaml
└── users/
    ├── users.txt        ← список пользователей (username password)
    └── check-auth.sh    ← скрипт авторизации, вызывается при каждом подключении
</pre>
 
=== 2.4 Система аутентификации пользователей ===
 
RU сервер использует <code>auth.type: command</code> — при каждом входящем подключении Hysteria2 вызывает внешний скрипт и передаёт ему пароль клиента в аргументах. Скрипт ищет пароль в файле <code>users.txt</code>: если совпадение найдено — выводит имя пользователя (оно попадает в логи) и завершается с кодом 0. Если нет — завершается с кодом 1 (отказ в подключении).
 
Преимущество этого подхода: <code>users.txt</code> можно редактировать в любой момент без перезапуска контейнера. Изменения применяются немедленно.
 
==== 2.4.1 Файл пользователей: users/users.txt ====
 
Формат: одна строка на пользователя — имя пользователя и пароль, разделённые пробелом.
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
openssl x509 -in /etc/sing-box/tls-ru/ru.crt -noout -subject -dates -ext subjectAltName
# Сгенерировать первого пользователя (пример: "default")
PASS=$(openssl rand -base64 18)
echo "default $PASS" > /opt/hysteria-ru/users/users.txt
echo "Пароль пользователя default: $PASS"
</syntaxhighlight>
</syntaxhighlight>


=== 2.3.4 (Опционально) SHA-256 fingerprint для клиентских ссылок ===
Формат файла:
<pre>
username1 пароль1
username2 пароль2
</pre>
 
'''Важно:''' не используй пробелы внутри пароля — пароль считывается по второму полю строки.
 
==== 2.4.2 Скрипт авторизации: users/check-auth.sh ====
 
Hysteria2 вызывает скрипт с тремя аргументами: <code>$1</code> — адрес клиента, <code>$2</code> — пароль, <code>$3</code> — tx bandwidth.
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
openssl x509 -in /etc/sing-box/tls-ru/ru.crt -noout -fingerprint -sha256
cat > /opt/hysteria-ru/users/check-auth.sh << 'EOF'
#!/bin/sh
AUTH_PAYLOAD="$2"
USERS_FILE="/etc/hysteria/users/users.txt"
 
USERNAME=$(awk -v pwd="$AUTH_PAYLOAD" '$2 == pwd {print $1; exit}' "$USERS_FILE" 2>/dev/null)
 
if [ -n "$USERNAME" ]; then
    echo "$USERNAME"
    exit 0
fi
exit 1
EOF
 
chmod +x /opt/hysteria-ru/users/check-auth.sh
</syntaxhighlight>
</syntaxhighlight>


== 2.4 Конфиг sing-box и systemd ==
Почему используется <code>awk</code>, а не <code>grep</code>:
* Hysteria2 использует Docker-образ на базе Alpine Linux с BusyBox.
* BusyBox <code>grep</code> не поддерживает флаг <code>-P</code> (Perl-совместимые регулярки).
* <code>awk</code> доступен во всех Unix/Linux окружениях и работает стабильно.
 
=== 2.5 Конфигурация сервера: server.yaml ===


=== 2.4.1 Задать переменные (выполнять в одной SSH-сессии) ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
export HY2_RU_IP="81.30.105.134"
cat > /opt/hysteria-ru/server.yaml << 'EOF'
export HY2_RU_CLIENT_PORT=40000
listen: :2053
export HY2_EU_IP="148.253.213.5"
 
export HY2_EU_PORT=40001
tls:
</syntaxhighlight>
  cert: /etc/hysteria/certs/your-hy2-ru-domain.example.com.crt
  key: /etc/hysteria/certs/your-hy2-ru-domain.example.com.key
 
auth:
  type: command
  command: /etc/hysteria/users/check-auth.sh


=== 2.4.2 Ввести пароль линка РФ→EU (вслепую, не попадёт в history) ===
ignoreClientBandwidth: true
<syntaxhighlight lang="bash">
read -s HY2_EU_PW
export HY2_EU_PW
echo "HY2_EU_PW_len=${#HY2_EU_PW}"
</syntaxhighlight>Введите пароль из шага [[Hysteria 2 каскад#1.4.2 Сгенерировать пароль линка РФ→EU|1.4.2]] вслепую и нажмите Enter.


=== 2.4.3 Сгенерировать пароль «клиент → РФ» (новый, сохранить) ===
masquerade:
<syntaxhighlight lang="bash">
  type: proxy
export HY2_RU_CLIENT_PW="$(openssl rand -base64 24 | tr -d '\n')"
  proxy:
echo "Сохраните пароль КЛИЕНТ→РФ (длина=${#HY2_RU_CLIENT_PW}):"
    url: https://news.ycombinator.com/
echo "${HY2_RU_CLIENT_PW}"
    rewriteHost: true
</syntaxhighlight>Скопируйте пароль в менеджер паролей. '''Не''' вставляйте в чаты.


=== 2.4.4 Записать /etc/sing-box/config.json ===
outbounds:
<syntaxhighlight lang="bash">
  - name: chain
python3 - <<'PY'
    type: socks5
import json, os
    socks5:
from pathlib import Path
      addr: 127.0.0.1:10810


cfg = {
acl:
    "log": {"level": "info", "timestamp": True},
  inline:
    "inbounds": [{
     - "chain(all)"
        "type": "hysteria2",
EOF
        "tag": "hy2-in",
        "listen": "::",
        "listen_port": int(os.environ["HY2_RU_CLIENT_PORT"]),
        "users": [{"name": "client1", "password": os.environ["HY2_RU_CLIENT_PW"]}],
        "tls": {
            "enabled": True,
            "certificate_path": "/etc/sing-box/tls-ru/ru.crt",
            "key_path": "/etc/sing-box/tls-ru/ru.key",
        },
        "masquerade": {"type": "proxy", "url": "https://news.ycombinator.com/", "rewrite_host": True},
    }],
    "outbounds": [
        {"type": "direct", "tag": "direct"},
        {
            "type": "hysteria2",
            "tag": "hy2-eu",
            "server": os.environ["HY2_EU_IP"],
            "server_port": int(os.environ["HY2_EU_PORT"]),
            "password": os.environ["HY2_EU_PW"],
            "tls": {"enabled": True, "server_name": os.environ["HY2_EU_IP"], "insecure": True},
        },
     ],
    "route": {"rules": [], "final": "hy2-eu"},
}
path = Path("/etc/sing-box/config.json")
path.write_text(json.dumps(cfg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
path.chmod(0o600)
print("OK:", path)
PY
</syntaxhighlight>
</syntaxhighlight>


=== 2.4.5 Проверить синтаксис конфига ===
Описание ключевых параметров:
<syntaxhighlight lang="bash">
 
sing-box check -c /etc/sing-box/config.json
{| class="wikitable"
echo "exit_code=$?"
|-
</syntaxhighlight>'''Ожидаемо:''' пустой вывод и <code>exit_code=0</code>.
! Параметр !! Значение !! Пояснение
|-
|| <code>auth.type: command</code> || Внешний скрипт || При каждом подключении вызывается <code>check-auth.sh</code>
|-
|| <code>auth.command</code> || Путь внутри контейнера || Скрипт смонтирован через volume: <code>./users:/etc/hysteria/users</code>
|-
|| <code>outbounds.chain</code> || SOCKS5 на 127.0.0.1:10810 || Трафик → hysteria-ru-client → EU
|-
|| <code>acl: chain(all)</code> || Весь трафик через chain || Синтаксис: имя outbound, затем адрес в скобках
|}
 
=== 2.6 Конфигурация клиента: client.yaml ===
 
RU клиент подключается к EU серверу и поднимает SOCKS5 на <code>127.0.0.1:10810</code>.


=== 2.4.6 Создать systemd-юнит ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
tee /etc/systemd/system/sing-box-hy2relay-test.service >/dev/null <<'EOF'
cat > /opt/hysteria-ru/client.yaml << 'EOF'
[Unit]
server: your-hy2-eu-domain.example.com:2053
Description=sing-box Hy2 relay (RU client -> EU exit) test
After=network-online.target
Wants=network-online.target


[Service]
auth: EU_HY2_PASSWORD
Type=simple
ExecStart=/usr/local/bin/sing-box run -c /etc/sing-box/config.json
Restart=on-failure
RestartSec=2
LimitNOFILE=1048576


[Install]
socks5:
WantedBy=multi-user.target
  listen: 127.0.0.1:10810
EOF
EOF
</syntaxhighlight>
</syntaxhighlight>


=== 2.4.7 Включить и запустить ===
Замени <code>EU_HY2_PASSWORD</code> на тот же пароль, который указан в <code>server.yaml</code> EU сервера.
 
=== 2.7 Конфигурация Docker Compose: docker-compose.yml ===
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl daemon-reload
cat > /opt/hysteria-ru/docker-compose.yml << 'EOF'
systemctl enable sing-box-hy2relay-test.service
services:
systemctl restart sing-box-hy2relay-test.service
  hysteria-ru-server:
    image: tobyxdd/hysteria:latest
    container_name: hysteria-ru-server
    restart: unless-stopped
    network_mode: host
    command: server --config /etc/hysteria/server.yaml
    volumes:
      - ./server.yaml:/etc/hysteria/server.yaml:ro
      - ./users:/etc/hysteria/users:ro
      - /var/lib/docker/volumes/caddy-naive_caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/your-hy2-ru-domain.example.com:/etc/hysteria/certs:ro
 
  hysteria-ru-client:
    image: tobyxdd/hysteria:latest
    container_name: hysteria-ru-client
    restart: unless-stopped
    network_mode: host
    command: client --config /etc/hysteria/client.yaml
    volumes:
      - ./client.yaml:/etc/hysteria/client.yaml:ro
EOF
</syntaxhighlight>
</syntaxhighlight>


=== 2.4.8 Статус ===
'''Критически важные моменты:'''
* Директория <code>./users</code> монтируется в оба контейнера... нет, только в <code>hysteria-ru-server</code>. Клиентский контейнер видит только свой <code>client.yaml</code>.
* При изменении <code>docker-compose.yml</code> (добавлении volume) нужен пересоздать контейнер: <code>docker compose down && docker compose up -d</code>. Простой <code>restart</code> не применяет изменения volumes.
 
=== 2.8 Первый запуск ===
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl --no-pager -l status sing-box-hy2relay-test.service
cd /opt/hysteria-ru
</syntaxhighlight>'''Ожидаемо:''' <code>Active: active (running)</code>, строка <code>udp server started at [::]:40000</code>.
 
# Скачать образ
docker compose pull


=== 2.4.9 UDP-порт ===
# Запустить оба контейнера
<syntaxhighlight lang="bash">
docker compose up -d
ss -ulpen | grep ':40000' || echo "НЕ СЛУШАЕТ"
 
# Подождать и проверить логи
sleep 3 && docker compose logs
</syntaxhighlight>
</syntaxhighlight>


== 2.5 Тест каскада на РФ-VPS (ожидается IP EU) ==
Ожидаемый вывод:
<pre>
hysteria-ru-server  | INFO  server mode
hysteria-ru-server  | INFO  server up and running  {"listen": ":2053"}
hysteria-ru-client  | INFO  client mode
hysteria-ru-client  | INFO  connected to server  {"udpEnabled": true, "count": 1}
hysteria-ru-client  | INFO  SOCKS5 server listening  {"addr": "127.0.0.1:10810"}
</pre>


=== 2.5.1 Установить hysteria-клиент (если нет) ===
=== 2.9 Проверка цепочки с RU сервера ===
<syntaxhighlight lang="bash">
command -v hysteria || {
  export HY2_TAG="app/v2.8.1"
  curl -4fL --connect-timeout 20 --max-time 300 --retry 8 --retry-delay 2 --retry-all-errors \
    -o /tmp/hysteria "https://github.com/apernet/hysteria/releases/download/${HY2_TAG}/hysteria-linux-amd64"
  install -m 0755 /tmp/hysteria /usr/local/bin/hysteria
  rm -f /tmp/hysteria
}
hysteria version
</syntaxhighlight>


=== 2.5.2 Прочитать пароль «клиент→РФ» из config.json ===
Тест: запустить временный Hysteria2 клиент на RU сервере и проверить, что трафик выходит через WARP (IP Cloudflare):
<syntaxhighlight lang="bash">
RUCL_PW="$(python3 - <<'PY'
import json
from pathlib import Path
cfg = json.loads(Path("/etc/sing-box/config.json").read_text(encoding="utf-8"))
print(cfg["inbounds"][0]["users"][0]["password"])
PY
)"
echo "RUCL_PW_len=${#RUCL_PW}"
</syntaxhighlight>


=== 2.5.3 Временный клиентский конфиг ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
cat > /tmp/hy2-ru-selftest.yaml <<EOF
# Создать временный конфиг тест-клиента
server: 127.0.0.1:40000
cat > /tmp/hy2-test.yaml << EOF
auth: ${RUCL_PW}
server: 127.0.0.1:2053
auth: $(awk 'NR==1{print $2}' /opt/hysteria-ru/users/users.txt)
tls:
tls:
   insecure: true
   insecure: true
  sni: your-hy2-ru-domain.example.com
socks5:
socks5:
   listen: 127.0.0.1:19900
   listen: 127.0.0.1:11080
EOF
EOF
</syntaxhighlight>


=== 2.5.4 Запуск и проверка IP ===
# Запустить тест-клиент
<syntaxhighlight lang="bash">
docker run -d --name hy2-test --network host \
hysteria client -c /tmp/hy2-ru-selftest.yaml >/tmp/hy2-ru-selftest.log 2>&1 &
  -v /tmp/hy2-test.yaml:/etc/hysteria/client.yaml:ro \
sleep 1
  tobyxdd/hysteria:latest client --config /etc/hysteria/client.yaml
curl -4fsS --max-time 20 --socks5-hostname 127.0.0.1:19900 https://api.ipify.org; echo
 
</syntaxhighlight>'''Ожидаемо:''' <code>148.253.213.5</code> — публичный IP EU, '''не''' РФ. Это означает, что трафик прошёл через EU.
# Ждать 3 секунды
sleep 3
 
# Проверить IP выхода — должен быть Cloudflare WARP IP
echo "IP через цепочку:"
curl -s --max-time 15 -x socks5h://127.0.0.1:11080 https://ifconfig.me


=== 2.5.5 Остановить и убрать ===
# Очистить
<syntaxhighlight lang="bash">
docker rm -f hy2-test
pkill -f "/tmp/hy2-ru-selftest.yaml" || true
rm -f /tmp/hy2-test.yaml
rm -f /tmp/hy2-ru-selftest.yaml /tmp/hy2-ru-selftest.log
</syntaxhighlight>
</syntaxhighlight>
----


= Часть 3. Генерация ссылок для клиентов =
Если IP вывода принадлежит Cloudflare (начинается с <code>2a09:</code> для IPv6 или относится к диапазонам WARP) — цепочка RU → EU → WARP работает корректно.
Клиент подключается '''только к РФ-серверу'''. Пароль — «клиент→РФ» (<code>inbounds[0].users[0].password</code> в <code>config.json</code> на РФ).


== 3.1 Стандартный URI hysteria2:// ==
'''Примечание:''' параметр <code>insecure: true</code> и <code>sni</code> нужны в тест-клиенте, потому что он подключается по IP (<code>127.0.0.1</code>), а сертификат выдан на домен. Реальные клиенты подключаются по домену и такие параметры не требуются.
Формат по [https://v2.hysteria.network/docs/developers/URI-Scheme/ официальной спецификации]:<pre>
hysteria2://ПАРОЛЬ@IP_РФ:ПОРТ/?insecure=1&sni=IP_РФ
</pre>Если пароль содержит символы <code>/</code>, <code>+</code>, <code>=</code> и т.п. — их нужно percent-encode.


=== 3.1.1 Сгенерировать URI на РФ-VPS (Python автоматически кодирует пароль) ===
----
<syntaxhighlight lang="bash">
python3 - <<'PY'
import json, urllib.parse
from pathlib import Path


cfg = json.loads(Path("/etc/sing-box/config.json").read_text(encoding="utf-8"))
== Часть 3: Управление пользователями (hy2-users.sh) ==
pw  = cfg["inbounds"][0]["users"][0]["password"]
ip  = "81.30.105.134"  # публичный IP РФ — замените на свой
port = cfg["inbounds"][0]["listen_port"]


uri = f"hysteria2://{urllib.parse.quote(pw, safe='')}@{ip}:{port}/?insecure=1&sni={ip}"
=== 3.1 Что это и зачем ===
print("\n=== URI для импорта в NekoBox / Hiddify / V2Box ===")
print(uri)
print("===")
PY
</syntaxhighlight>'''Скопируйте URI''' — его можно:


* вставить в NekoBox → '''Добавить профиль → Из буфера обмена''';
Для управления пользователями Hysteria2 на RU сервере предназначен скрипт <code>hy2-users.sh</code>. Он предоставляет интерактивное меню с набором операций над файлом <code>users.txt</code>.
* вставить в Hiddify → '''Новый профиль''';
* импортировать в V2Box.


=== 3.1.2 URI с pinSHA256 вместо insecure (более безопасно) ===
Скрипт решает следующие задачи:
Сначала получить fingerprint (на EU-сервере или РФ, зависит от чьего сертификата):<syntaxhighlight lang="bash">
openssl x509 -in /etc/sing-box/tls-ru/ru.crt -noout -fingerprint -sha256 \
  | sed 's/SHA256 Fingerprint=//;s/://g' | tr '[:upper:]' '[:lower:]'
</syntaxhighlight>Пример URI с pin (замените <code>FINGERPRINT_HEX</code>):<pre>
hysteria2://ПАРОЛЬ@81.30.105.134:40000/?pinSHA256=FINGERPRINT_HEX&sni=81.30.105.134
</pre>


=== 3.1.3 Сгенерировать QR-код (текстовый, в терминале) ===
<syntaxhighlight lang="bash">
apt-get install -y qrencode
qrencode -t ANSIUTF8 'ВСТАВЬТЕ_URI_СЮДА'
</syntaxhighlight>'''Примечание:''' пароль будет виден в QR и в истории shell — делайте это только на изолированной сессии или перенаправляйте вывод в файл PNG и передавайте по защищённому каналу.
== 3.2 Параметры для ручного добавления в NekoBox ==
{| class="wikitable"
{| class="wikitable"
!Поле
!Значение
|-
|Тип
|Hysteria 2 (именно Hy2, не Hysteria 1)
|-
|Сервер
|<code>81.30.105.134</code>
|-
|-
|Порт
! Операция !! Описание
|<code>40000</code>
|-
|-
|Пароль
|| Список пользователей || Показывает всех пользователей из users.txt с маскированными паролями
|пароль «клиент→РФ» из <code>/etc/sing-box/config.json</code>
|-
|-
|TLS → Insecure
|| Добавить пользователя || Запрашивает имя, автоматически генерирует криптостойкий пароль, сохраняет в users.txt, показывает готовый Hysteria2 URI
|включить (или задать pin)
|-
|-
|SNI
|| Удалить пользователя || Интерактивный выбор из списка, удаление строки из users.txt
|<code>81.30.105.134</code>
|-
|-
|Obfs
|| Показать конфиг || Генерирует и показывает Hysteria2 URI для выбранного пользователя
|'''не включать''' (salamander не настроен)
|}
|}


; Ожидаемый IP при проверке
Ключевые особенности:
: Внешний IP должен быть '''европейским''' (<code>148.253.213.5</code>), а не российским.
* '''Нет перезапуска контейнера.''' Hysteria2 с <code>auth.type: command</code> читает файл при каждом подключении — изменения в <code>users.txt</code> применяются мгновенно.
* '''Пароли генерируются автоматически''' через <code>openssl rand</code> — случайные, криптостойкие.
* '''URI формат''' соответствует стандарту Hysteria2 и принимается всеми клиентами: NekoBox, husi, Exclave, podkop.
 
=== 3.2 Установка скрипта ===
 
Скрипт хранится в репозитории по пути <code>scripts/hy2-users.sh</code>. Установка на сервер:


----
<syntaxhighlight lang="bash">
# Скопировать скрипт на сервер
scp scripts/hy2-users.sh root@YOUR_RU_SERVER_IP:/usr/local/bin/hy2-users.sh


= Часть 4. Обслуживание серверов =
# Сделать исполняемым
ssh root@YOUR_RU_SERVER_IP "chmod +x /usr/local/bin/hy2-users.sh"
</syntaxhighlight>


== 4.1 Просмотр статуса сервисов ==
Или выполнить непосредственно на RU сервере (если скрипт уже там):


=== EU ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl --no-pager -l status hysteria-eu-hy2test.service
chmod +x /usr/local/bin/hy2-users.sh
</syntaxhighlight>
</syntaxhighlight>


=== РФ ===
'''Проверка переменных в начале скрипта.''' Открой скрипт и убедись, что следующие переменные указывают на правильные значения:
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl --no-pager -l status sing-box-hy2relay-test.service
head -15 /usr/local/bin/hy2-users.sh
</syntaxhighlight>
</syntaxhighlight>


== 4.2 Просмотр логов ==
Должны быть:
* <code>USERS_FILE="/opt/hysteria-ru/users/users.txt"</code>
* <code>DOMAIN="your-hy2-ru-domain.example.com"</code>
* <code>PORT="2053"</code>


=== EU (последние 60 строк) ===
Если домен указан неверно — исправь:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
journalctl -u hysteria-eu-hy2test -n 60 --no-pager
sed -i 's|DOMAIN=.*|DOMAIN="your-hy2-ru-domain.example.com"|' /usr/local/bin/hy2-users.sh
</syntaxhighlight>
</syntaxhighlight>


=== РФ (последние 60 строк) ===
=== 3.3 Использование ===
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
journalctl -u sing-box-hy2relay-test -n 60 --no-pager
sudo bash /usr/local/bin/hy2-users.sh
</syntaxhighlight>
</syntaxhighlight>


=== Хвост в реальном времени (EU) ===
Скрипт показывает меню:
<pre>
┌─────────────────────────────────────────────┐
│  Hysteria2 — управление пользователями RU  │
└─────────────────────────────────────────────┘
 
  1. Список пользователей
  2. Добавить пользователя
  3. Удалить пользователя
  4. Показать конфиг клиента
 
  0. Выход
 
Выбери действие:
</pre>
 
При добавлении нового пользователя скрипт выводит готовый URI:
<pre>
hysteria2://СГЕНЕРИРОВАННЫЙ_ПАРОЛЬ@your-hy2-ru-domain.example.com:2053
</pre>
 
Этот URI вставляется напрямую в клиентское приложение (см. Часть 4).
 
=== 3.4 Исходный код скрипта ===
 
<div class="mw-collapsible mw-collapsed" style="width:100%">
<div style="font-weight:bold; padding:4px 0;">▶ Показать исходный код hy2-users.sh</div>
<div class="mw-collapsible-content">
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
journalctl -u hysteria-eu-hy2test -f
#!/bin/bash
# =============================================================
# hy2-users — управление пользователями Hysteria2 на RU сервере
# users.txt: /opt/hysteria-ru/users/users.txt
# Формат: username password (по одной строке)
# Перезапуск контейнера НЕ требуется — изменения применяются мгновенно
# =============================================================
 
USERS_FILE="/opt/hysteria-ru/users/users.txt"
DOMAIN="your-hy2-ru-domain.example.com"
PORT="2053"
 
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
 
# --- Вспомогательные функции ---
 
check_root() {
    if [[ $EUID -ne 0 ]]; then
        echo -e "${RED}Ошибка: запусти скрипт от root (sudo bash hy2-users.sh)${NC}"
        exit 1
    fi
}
 
check_users_file() {
    if [[ ! -f "$USERS_FILE" ]]; then
        echo -e "${RED}Ошибка: файл пользователей не найден: $USERS_FILE${NC}"
        exit 1
    fi
}
 
get_usernames() {
    awk '{print $1}' "$USERS_FILE"
}
 
get_password() {
    local user="$1"
    awk -v u="$user" '$1 == u {print $2}' "$USERS_FILE"
}
 
press_enter() {
    echo ""
    read -rp "Нажми Enter для возврата в меню..."
}
 
print_config_for() {
    local user="$1"
    local pass="$2"
    echo ""
    echo -e "${BOLD}┌─────────────────────────────────────────────┐${NC}"
    echo -e "${BOLD}│  Hysteria2 URI для клиента                  │${NC}"
    echo -e "${BOLD}└─────────────────────────────────────────────┘${NC}"
    echo ""
    echo -e "${BOLD}  hysteria2://${pass}@${DOMAIN}:${PORT}${NC}"
    echo ""
    echo -e "${CYAN}Для podkop (OpenWrt):${NC}  вставить URI как hy2 outbound"
    echo -e "${CYAN}Для мобильных клиентов:${NC}  NekoBox, husi, Exclave — импорт URI"
    echo -e "${CYAN}Пользователь:${NC}  ${user}"
}
 
# --- Функции меню ---
 
list_users() {
    echo ""
    echo -e "${BOLD}${CYAN}=== Список пользователей ===${NC}"
    mapfile -t users < <(get_usernames)
    if [[ ${#users[@]} -eq 0 ]]; then
        echo -e "${YELLOW}Пользователей нет.${NC}"
    else
        for i in "${!users[@]}"; do
            echo -e "  ${BOLD}$((i+1)).${NC} ${users[$i]}"
        done
        echo ""
        echo -e "  Всего: ${#users[@]} пользователь(ей)"
    fi
    press_enter
}
 
add_user() {
    echo ""
    echo -e "${BOLD}${CYAN}=== Добавить пользователя ===${NC}"
 
    read -rp "Имя пользователя: " username
    username=$(echo "$username" | tr -d ' ')
 
    if [[ -z "$username" ]]; then
        echo -e "${RED}Имя не может быть пустым.${NC}"
        press_enter
        return
    fi
 
    if grep -q "^${username} " "$USERS_FILE" 2>/dev/null; then
        echo -e "${RED}Пользователь '${username}' уже существует.${NC}"
        press_enter
        return
    fi
 
    read -rp "Пароль (Enter = сгенерировать автоматически): " password
    if [[ -z "$password" ]]; then
        password=$(openssl rand -base64 18 | tr -d '=/+' | head -c 22)
        echo -e "  Сгенерирован пароль: ${BOLD}${password}${NC}"
    fi
 
    echo "${username} ${password}" >> "$USERS_FILE"
    echo -e "${GREEN}Пользователь '${username}' добавлен.${NC}"
    echo -e "${YELLOW}Изменения применяются мгновенно — перезапуск не нужен.${NC}"
 
    print_config_for "$username" "$password"
    press_enter
}
 
delete_user() {
    echo ""
    echo -e "${BOLD}${CYAN}=== Удалить пользователя ===${NC}"
    mapfile -t users < <(get_usernames)
 
    if [[ ${#users[@]} -eq 0 ]]; then
        echo -e "${YELLOW}Нет пользователей для удаления.${NC}"
        press_enter
        return
    fi
 
    for i in "${!users[@]}"; do
        echo -e "  ${BOLD}$((i+1)).${NC} ${users[$i]}"
    done
    echo "  0. Отмена"
    echo ""
    read -rp "Выбери номер для удаления: " choice
 
    if [[ "$choice" == "0" || -z "$choice" ]]; then
        return
    fi
 
    if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#users[@]} )); then
        echo -e "${RED}Неверный номер.${NC}"
        press_enter
        return
    fi
 
    target="${users[$((choice-1))]}"
    read -rp "Удалить пользователя '${target}'? (y/N): " confirm
    if [[ "$confirm" =~ ^[Yy]$ ]]; then
        sed -i "/^${target} /d" "$USERS_FILE"
        echo -e "${GREEN}Пользователь '${target}' удалён.${NC}"
        echo -e "${YELLOW}Изменения применяются мгновенно — перезапуск не нужен.${NC}"
    else
        echo "Отменено."
    fi
    press_enter
}
 
show_config() {
    echo ""
    echo -e "${BOLD}${CYAN}=== URI клиента ===${NC}"
    mapfile -t users < <(get_usernames)
 
    if [[ ${#users[@]} -eq 0 ]]; then
        echo -e "${YELLOW}Нет пользователей.${NC}"
        press_enter
        return
    fi
 
    for i in "${!users[@]}"; do
        echo -e "  ${BOLD}$((i+1)).${NC} ${users[$i]}"
    done
    echo "  0. Отмена"
    echo ""
    read -rp "Выбери номер пользователя: " choice
 
    if [[ "$choice" == "0" || -z "$choice" ]]; then
        return
    fi
 
    if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#users[@]} )); then
        echo -e "${RED}Неверный номер.${NC}"
        press_enter
        return
    fi
 
    local user="${users[$((choice-1))]}"
    local pass
    pass=$(get_password "$user")
    print_config_for "$user" "$pass"
    press_enter
}
 
# --- Главное меню ---
 
main_menu() {
    check_root
    check_users_file
 
    while true; do
        clear
        echo -e "${BOLD}${CYAN}"
        echo "╔══════════════════════════════════════╗"
        echo "║  Hysteria2 User Manager (RU)        ║"
        echo "║  Домен: ${DOMAIN}  ║"
        echo "╚══════════════════════════════════════╝"
        echo -e "${NC}"
 
        mapfile -t users < <(get_usernames)
        echo -e "  Активных пользователей: ${BOLD}${#users[@]}${NC}"
        echo ""
        echo -e "  ${BOLD}1.${NC} Список пользователей"
        echo -e "  ${BOLD}2.${NC} Добавить пользователя"
        echo -e "  ${BOLD}3.${NC} Удалить пользователя"
        echo -e "  ${BOLD}4.${NC} Показать URI клиента"
        echo -e "  ${BOLD}0.${NC} Выход"
        echo ""
        read -rp "Выбор: " option
 
        case "$option" in
            1) list_users ;;
            2) add_user ;;
            3) delete_user ;;
            4) show_config ;;
            0) echo "Выход."; exit 0 ;;
            *) echo -e "${RED}Неверный выбор.${NC}"; sleep 1 ;;
        esac
    done
}
 
main_menu
 
</syntaxhighlight>
</syntaxhighlight>
</div>
</div>
----
== Часть 4: Настройка клиентов ==
=== 4.1 Формат URI Hysteria2 ===
Hysteria2 использует стандартизированный URI для передачи параметров подключения клиентам:
<pre>
hysteria2://ПАРОЛЬ@ДОМЕН:ПОРТ
</pre>
Пример:
<pre>
hysteria2://aBcDeFgHiJkLmNoPqRsT@your-hy2-ru-domain.example.com:2053
</pre>
URI можно получить через скрипт <code>hy2-users.sh</code> (пункт 4 меню — «Показать конфиг клиента»).
=== 4.2 Мобильные клиенты (iOS / Android) ===


== 4.3 Узнать текущие пароли ==
Рекомендуемые приложения с нативной поддержкой Hysteria2:


=== Пароль линка РФ→EU (читать на EU) ===
{| class="wikitable"
<syntaxhighlight lang="bash">
|-
python3 - <<'PY'
! Приложение !! Платформа !! Как импортировать URI
import re
|-
from pathlib import Path
|| NekoBox || Android || Главный экран → + → Add → Hysteria2 → вставить URI
text = Path("/etc/hysteria/server-eu.yaml").read_text(encoding="utf-8")
|-
m = re.search(r"(?m)^\s*password:\s*'([^']*)'\s*$", text)
|| husi || Android || Профили → + → Hysteria2 → вставить URI
print("EU_PASSWORD:", m.group(1) if m else "NOT FOUND")
|-
PY
|| Exclave || iOS || Конфигурации → + → Hysteria2 → вставить URI
|-
|| Shadowrocket || iOS || Главная → + → Тип: Hysteria2 → вставить URI
|}
 
Все эти приложения принимают URI в формате <code>hysteria2://...</code> напрямую — через кнопку импорта, QR-код или буфер обмена.
 
=== 4.3 ПК-клиенты (Windows / macOS / Linux) ===
 
==== Вариант А: официальный бинарь Hysteria2 ====
 
Скачать бинарь с официального репозитория: https://github.com/apernet/hysteria/releases
 
Создать файл конфигурации <code>config.yaml</code>:
<syntaxhighlight lang="yaml">
server: your-hy2-ru-domain.example.com:2053
 
auth: ВАШ_ПАРОЛЬ
 
socks5:
  listen: 127.0.0.1:1080
 
http:
  listen: 127.0.0.1:8080
</syntaxhighlight>
</syntaxhighlight>


=== Пароль «клиент→РФ» (читать на РФ) ===
Запустить:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
python3 - <<'PY'
# Linux / macOS
import json
./hysteria client --config config.yaml
from pathlib import Path
 
cfg = json.loads(Path("/etc/sing-box/config.json").read_text(encoding="utf-8"))
# Windows (PowerShell)
print("CLIENT_TO_RU_PASSWORD:", cfg["inbounds"][0]["users"][0]["password"])
.\hysteria.exe client --config config.yaml
PY
</syntaxhighlight>
</syntaxhighlight>


=== Пароль «РФ→EU» (читать на РФ) ===
После запуска:
* SOCKS5 прокси доступен на <code>127.0.0.1:1080</code>
* HTTP прокси доступен на <code>127.0.0.1:8080</code>
 
==== Вариант Б: через NekoRay / Hiddify Next (Windows / Linux) ====
 
Приложения NekoRay и Hiddify Next принимают Hysteria2 URI — импорт через меню или буфер обмена. Автоматически управляют запуском клиентского процесса.
 
=== 4.4 OpenWrt / podkop ===
 
podkop (реализация на базе sing-box) нативно поддерживает Hysteria2 URI. Никакой дополнительной установки бинарей не требуется.
 
В интерфейсе podkop (LuCI → Network → podkop → Outbound):
* Тип: <code>Hysteria2</code>
* URI: <code>hysteria2://ПАРОЛЬ@your-hy2-ru-domain.example.com:2053</code>
 
Или через UCI:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
python3 - <<'PY'
uci set podkop.main.proxy='hysteria2://ПАРОЛЬ@your-hy2-ru-domain.example.com:2053'
import json
uci commit podkop
from pathlib import Path
/etc/init.d/podkop restart
cfg = json.loads(Path("/etc/sing-box/config.json").read_text(encoding="utf-8"))
for ob in cfg["outbounds"]:
    if ob.get("type") == "hysteria2":
        print("RU_TO_EU_PASSWORD:", ob["password"])
PY
</syntaxhighlight>
</syntaxhighlight>


== 4.4 Ротация паролей (после отладки — обязательно) ==
После сохранения все устройства домашней сети автоматически получают обход без дополнительной настройки.
Если пароль засветился в чате или логах:
 
----
 
== Часть 5: Проверка всей цепочки ==
 
=== 5.1 Тест RU сервера: цепочка RU → EU → WARP ===
 
Выполнить на RU сервере — проверяет, что трафик выходит через WARP:


=== Шаг 1: новый пароль на EU ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
NEW_EU_PW="$(openssl rand -base64 32 | tr -d '\n')"
# SOCKS5 порт 10810 — это точка выхода RU клиента (= вход в EU сервер → WARP)
echo "Новый пароль EU (сохраните):"
curl -s --max-time 20 -x socks5h://127.0.0.1:10810 https://ifconfig.me
echo "${NEW_EU_PW}"
echo ""
</syntaxhighlight>


# Заменить в конфиге
Результат должен быть '''IP Cloudflare WARP''' — например, начинается с <code>2a09:</code> (IPv6) или принадлежит AS13335 (Cloudflare).
python3 - <<PY
import re
from pathlib import Path
p = Path("/etc/hysteria/server-eu.yaml")
text = p.read_text(encoding="utf-8")
text = re.sub(r"(password:\s*')[^']*(')", r"\g<1>${NEW_EU_PW}\g<2>", text)
p.write_text(text, encoding="utf-8")
print("Обновлено:", p)
PY
</syntaxhighlight>'''Или''' отредактируйте файл вручную:<syntaxhighlight lang="bash">
nano /etc/hysteria/server-eu.yaml
</syntaxhighlight>


=== Шаг 2: перезапустить EU ===
Проверить AS:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl restart hysteria-eu-hy2test.service
IP=$(curl -s --max-time 20 -x socks5h://127.0.0.1:10810 https://ifconfig.me)
systemctl is-active hysteria-eu-hy2test.service
curl -s "https://ipinfo.io/${IP}/org"
echo ""
</syntaxhighlight>
</syntaxhighlight>


=== Шаг 3: обновить пароль «РФ→EU» в конфиге РФ ===
Ожидаемый вывод: строка содержит <code>Cloudflare</code>.
<syntaxhighlight lang="bash">
 
# На РФ — отредактируйте поле "password" в блоке outbound hy2-eu
=== 5.2 Тест с реального клиента ===
python3 - <<'PY'
 
import json
После добавления пользователя через <code>hy2-users.sh</code> и получения URI:
from pathlib import Path
 
# Вставь URI в клиентское приложение (NekoBox, husi, Shadowrocket).
# Активируй профиль.
# Открой <code>https://ifconfig.me</code> в браузере — должен отображаться IP Cloudflare WARP.
# Открой <code>https://www.whatismybrowser.com/detect/what-is-my-ip-address</code> для проверки отсутствия WebRTC утечки.
 
=== 5.3 Диагностика проблем ===


new_pw = input("Введите новый пароль EU: ")
==== Проблема: hysteria-eu не запускается — ошибка TLS ====
path = Path("/etc/sing-box/config.json")
cfg = json.loads(path.read_text(encoding="utf-8"))
for ob in cfg["outbounds"]:
    if ob.get("tag") == "hy2-eu":
        ob["password"] = new_pw
        print(f"Обновлён outbound '{ob['tag']}'")
path.write_text(json.dumps(cfg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
path.chmod(0o600)
print("Сохранено:", path)
PY
</syntaxhighlight>


=== Шаг 4: перезапустить РФ ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl restart sing-box-hy2relay-test.service
docker compose -f /opt/hysteria-eu/docker-compose.yml logs hysteria-eu
systemctl is-active sing-box-hy2relay-test.service
</syntaxhighlight>
</syntaxhighlight>


=== Шаг 5: обновить URI клиентов ===
Возможные причины:
Заново выполнить [[Hysteria 2 каскад#3.1.1 Сгенерировать URI на РФ-VPS|раздел 3.1.1]] и передать новые ссылки на устройства.
* Путь к сертификату неверен → проверь <code>ls</code> по пути, указанному в volumes.
* Сертификат ещё не был получен Caddy → проверь, что caddy-naive запущен и прошёл ACME-проверку.
* Caddy хранит сертификат под другим именем → выполни <code>find /var/lib/docker/volumes/caddy-naive_caddy_data/_data -name "*.crt"</code>.


== 4.5 Перезапуск после правки конфигов ==
==== Проблема: hysteria-ru-server выдаёт auth error ====


=== EU ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl restart hysteria-eu-hy2test.service
docker compose -f /opt/hysteria-ru/docker-compose.yml logs hysteria-ru-server
</syntaxhighlight>
</syntaxhighlight>


=== РФ ===
Проверить вручную (на RU сервере):
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl restart sing-box-hy2relay-test.service
# Проверить, что скрипт находится на месте внутри контейнера
docker exec hysteria-ru-server ls -la /etc/hysteria/users/
 
# Запустить скрипт вручную с тестовым паролем
docker exec hysteria-ru-server /etc/hysteria/users/check-auth.sh addr ТЕСТОВЫЙ_ПАРОЛЬ 0
echo "Exit code: $?"
</syntaxhighlight>
</syntaxhighlight>


== 4.6 Остановка и отключение сервисов (при необходимости) ==
Если <code>Exit code: 0</code> — скрипт работает. Если <code>1</code> — пароль не найден в <code>users.txt</code>.
 
==== Проблема: hysteria-ru-client не подключается к EU ====


=== EU ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl disable --now hysteria-eu-hy2test.service
docker compose -f /opt/hysteria-ru/docker-compose.yml logs hysteria-ru-client
</syntaxhighlight>
</syntaxhighlight>


=== РФ ===
Возможные причины:
* <code>EU_HY2_PASSWORD</code> в <code>client.yaml</code> не совпадает с паролем в EU <code>server.yaml</code>.
* UDP порт 2053 заблокирован на EU сервере (firewall) → проверь <code>ss -ulnp | grep 2053</code> на EU сервере.
* EU сервер ещё не запущен.
 
==== Проблема: ACL ошибка при запуске ====
 
<pre>
FATAL failed to load server config: invalid config: acl.inline: invalid syntax at line 1
</pre>
 
Неверный синтаксис ACL. Правильный формат: <code>"outbound_name(address)"</code>:
* ✅ <code>"warp(all)"</code> — направить всё в outbound "warp"
* ✅ <code>"chain(all)"</code> — направить всё в outbound "chain"
* ❌ <code>"outbound(warp) all"</code> — неверно
 
----
 
== Часть 6: Обслуживание ==
 
=== 6.1 Управление контейнерами ===
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
systemctl disable --now sing-box-hy2relay-test.service
# EU сервер
cd /opt/hysteria-eu
docker compose up -d        # запустить
docker compose down          # остановить
docker compose restart      # перезапустить
docker compose logs -f      # следить за логами в реальном времени
docker compose ps            # статус
 
# RU сервер (оба контейнера)
cd /opt/hysteria-ru
docker compose up -d
docker compose down
docker compose restart
docker compose logs -f
 
# RU сервер (по отдельности)
docker compose restart hysteria-ru-server  # только сервер
docker compose restart hysteria-ru-client  # только клиент
docker compose logs -f hysteria-ru-server  # логи только сервера
docker compose logs -f hysteria-ru-client  # логи только клиента
</syntaxhighlight>
</syntaxhighlight>


== 4.7 Проверка занятости портов ==
=== 6.2 Обновление сертификатов ===


=== Смотреть все UDP-порты ===
TLS-сертификаты обновляются Caddy автоматически (за 30 дней до истечения). После автообновления Hysteria2 начнёт использовать новый сертификат при следующем перезапуске.
 
Для принудительного применения нового сертификата:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
ss -ulpen
# EU сервер
cd /opt/hysteria-eu && docker compose restart
 
# RU сервер
cd /opt/hysteria-ru && docker compose restart hysteria-ru-server
</syntaxhighlight>
</syntaxhighlight>


=== Конкретный порт EU ===
Проверить дату истечения текущего сертификата:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
ss -ulpen | grep ':40001'
# EU сервер
DOMAIN="your-hy2-eu-domain.example.com"
CERT="/var/lib/docker/volumes/caddy-naive_caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}/${DOMAIN}.crt"
openssl x509 -in "${CERT}" -noout -enddate
</syntaxhighlight>
</syntaxhighlight>


=== Конкретный порт РФ ===
=== 6.3 Обновление Docker-образа ===
 
Hysteria2 активно развивается. Для обновления до последней версии:
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
ss -ulpen | grep ':40000'
# EU сервер
cd /opt/hysteria-eu
docker compose pull          # скачать новый образ
docker compose up -d          # перезапустить с новым образом
docker compose logs --tail=5  # проверить старт
 
# RU сервер
cd /opt/hysteria-ru
docker compose pull
docker compose up -d
docker compose logs --tail=5
</syntaxhighlight>
</syntaxhighlight>


== 4.8 Проверка TLS сертификатов ==
Старые неиспользуемые образы можно удалить:
 
=== EU — когда истекает ===
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
openssl x509 -in /etc/hysteria/tls-eu/eu.crt -noout -dates
docker image prune -f
</syntaxhighlight>
</syntaxhighlight>


=== РФ — когда истекает ===
=== 6.4 Мониторинг логов ===
 
Hysteria2 пишет структурированные JSON-логи. Полезные паттерны для мониторинга:
 
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
openssl x509 -in /etc/sing-box/tls-ru/ru.crt -noout -dates
# Смотреть все подключения на RU сервере
</syntaxhighlight>Самоподписанные сертификаты выдаются на 10 лет. При необходимости перевыпустить — повторить [[Hysteria 2 каскад#1.3 TLS|раздел 1.3]] (EU) или [[Hysteria 2 каскад#2.3 TLS для входа Hy2|2.3]] (РФ), затем перезапустить сервис.
docker logs hysteria-ru-server 2>&1 | grep -E "connected|disconnected|auth"
----
 
# Смотреть ошибки
docker logs hysteria-ru-server 2>&1 | grep -i "error\|fatal\|warn"
 
# Считать количество уникальных пользователей (поле "id")
docker logs hysteria-ru-server 2>&1 | grep '"id"' | grep -oP '"id": "\K[^"]+' | sort -u
 
# Следить за логами EU сервера в реальном времени
docker logs -f hysteria-eu 2>&1
</syntaxhighlight>


= Часть 5. Типичные проблемы =
Пример нормального лога RU сервера при входящем подключении:
{| class="wikitable"
<pre>
!Симптом
INFO  client connected    {"addr": "CLIENT_IP:PORT", "id": "username"}
!Причина
INFO  TCP request          {"addr": "CLIENT_IP:PORT", "id": "username", "reqAddr": "example.com:443"}
!Что сделать
INFO  client disconnected  {"addr": "CLIENT_IP:PORT", "id": "username", "error": "..."}
|-
</pre>
|Сервис не стартует
|Занят порт, ошибка в конфиге, нет прав на ключ
|<code>journalctl -u ИМЯ_СЕРВИСА -n 80</code>; <code>ss -ulpen</code>; <code>sing-box check</code>
|-
|<code>curl</code> даёт IP РФ вместо EU
|Маршрут <code>final</code> не <code>hy2-eu</code>; EU недоступен с РФ по UDP
|Проверить <code>route.final</code> в <code>config.json</code>; пинг EU-порта вручную
|-
|Клиент не подключается
|Фаервол хостера режет UDP; неверный пароль; неверный SNI
|Проверить Security Group/Firewall у хостера; проверить пароль командой [[Hysteria 2 каскад#4.3 Узнать текущие пароли|4.3]]
|-
|TLS handshake failed
|Несовпадение SNI с CN/SAN сертификата; <code>insecure</code> не задан
|SNI в клиенте должен совпадать с IP, на который выдан серт; или включить <code>insecure=1</code>
|-
|<code>curl</code> к GitHub даёт 504
|Временный сбой GitHub CDN
|Повторить с <code>--retry 8</code>; первая попытка часто обрывается, следующая проходит
|-
|NekoBox — ошибка при импорте URI
|Пароль не закодирован (%, /,  +, = в пароле)
|Использовать Python-генератор URI из [[Hysteria 2 каскад#3.1.1 Сгенерировать URI|раздела 3.1.1]]
|}
----


= Часть 6. Справочник файлов и сервисов =
Поле <code>"id"</code> — это имя пользователя, возвращённое скриптом <code>check-auth.sh</code>. Это позволяет отслеживать активность конкретных пользователей в логах без хранения паролей.
{| class="wikitable"
!Сервер
!Файл / сервис
!Назначение
|-
|EU
|<code>/usr/local/bin/hysteria</code>
|Бинарь Hysteria v2.8.1
|-
|EU
|<code>/etc/hysteria/server-eu.yaml</code>
|Конфиг сервера (порт, TLS, пароль, masquerade)
|-
|EU
|<code>/etc/hysteria/tls-eu/eu.crt</code>
|Самоподписанный TLS-сертификат (CN=IP EU)
|-
|EU
|<code>/etc/hysteria/tls-eu/eu.key</code>
|Приватный ключ TLS (права 0640)
|-
|EU
|<code>hysteria-eu-hy2test.service</code>
|systemd-сервис Hysteria
|-
|РФ
|<code>/usr/local/bin/sing-box</code>
|Бинарь sing-box v1.13.8
|-
|РФ
|<code>/usr/local/bin/hysteria</code>
|Бинарь Hysteria (только для локальных тестов)
|-
|РФ
|<code>/etc/sing-box/config.json</code>
|Конфиг релея (inbound Hy2 + outbound Hy2 на EU)
|-
|РФ
|<code>/etc/sing-box/tls-ru/ru.crt</code>
|Самоподписанный TLS-сертификат (CN=IP РФ)
|-
|РФ
|<code>/etc/sing-box/tls-ru/ru.key</code>
|Приватный ключ TLS (права 0640)
|-
|РФ
|<code>sing-box-hy2relay-test.service</code>
|systemd-сервис sing-box
|}

Текущая версия от 10:14, 26 апреля 2026

Hysteria2: установка каскадной цепочки Entry Node → Exit Node → WARP

Что такое Hysteria2 и зачем он нужен

Проблема: ограничения традиционных прокси-протоколов

Современные системы глубокой инспекции пакетов (DPI — Deep Packet Inspection), применяемые интернет-провайдерами и регуляторами по всему миру, умеют анализировать не только адреса назначения, но и характер самого трафика. Даже зашифрованное соединение можно идентифицировать — по поведению, по паузам между пакетами, по размерам передаваемых блоков данных, по TLS-fingerprint.

Большинство прокси-протоколов работают поверх TCP. Это означает:

  • Все потери пакетов обрабатываются дважды — сначала на уровне транспорта, затем внутри туннеля (head-of-line blocking).
  • При нестабильном канале (мобильная сеть, спутник, VPN на слабом железе) скорость резко падает.
  • TCP-fingerprint легче распознаётся и блокируется.

Hysteria2 решает эту проблему фундаментально иначе: он работает поверх QUIC — транспортного протокола на базе UDP, разработанного в Google и стандартизированного в RFC 9000.

Решение: QUIC/UDP с маскировкой под HTTPS

Hysteria2 строит зашифрованный туннель поверх UDP, используя QUIC:

  • Нет head-of-line blocking: потеря одного пакета не блокирует остальные потоки.
  • Быстрое восстановление соединения: QUIC переподключается за 0-RTT (без дополнительного round-trip).
  • Маскировка под HTTPS: снаружи трафик Hysteria2 выглядит как обычный QUIC/HTTP3 браузерный трафик — сервер «притворяется» обычным HTTPS-сайтом и возвращает реальный веб-контент при неавторизованных запросах.
  • Congestion control: поддерживает несколько алгоритмов управления перегрузкой, включая BBR.
Протокол Транспорт Как выглядит для DPI Устойчивость к блокировке Скорость при потерях
Обычный VPN (WireGuard, OpenVPN) UDP / TCP Характерный шаблон Низкая — fingerprint легко детектируется Высокая (WG) / Низкая (OpenVPN)
Shadowsocks TCP Случайный зашифрованный поток Средняя — выявляется по энтропии Средняя
VLESS + REALITY TCP (TLS) TLS с реальным сертификатом стороннего сайта Высокая Средняя
NaïveProxy TCP (HTTP/2 TLS) Неотличим от Chrome HTTPS Очень высокая Средняя (TCP Head-of-Line)
Hysteria2 UDP (QUIC/TLS) QUIC/HTTP3, маскировка под HTTPS Очень высокая Высокая — устойчив к потерям

Как работает Hysteria2 технически

Hysteria2 использует протокол QUIC в качестве транспорта:

  1. Клиент устанавливает QUIC-соединение с сервером — это TLS 1.3 поверх UDP.
  2. Внутри QUIC-соединения открываются независимые потоки (streams) для каждого проксируемого соединения.
  3. Сервер обрабатывает входящий трафик: авторизует клиента (по паролю или через внешний скрипт), затем перенаправляет трафик через указанный outbound.
  4. Если к серверу обращаются без правильного пароля — сервер ведёт себя как обычный HTTPS-сайт (masquerade) и возвращает реальный контент с заданного URL.

Особенности аутентификации в данной схеме:

  • EU сервер: использует одиночный пароль — только для входящих соединений от RU клиента (не клиентских устройств).
  • RU сервер: использует command-аутентификацию — при каждом подключении вызывается внешний bash-скрипт, который проверяет пароль по файлу users.txt. Это позволяет добавлять и удалять пользователей без перезапуска сервера.

Каскадная цепочка: зачем два сервера

В данном решении используется каскад из двух серверов: Entry Node (ближайший к клиентам сервер-вход) и Exit Node (сервер-выход в интернет). Это архитектурное решение принято намеренно по нескольким причинам:

  • Снижение риска для клиента: клиентские устройства подключаются только к ближайшему серверу. Соединение с соседним узлом менее заметно, чем прямое соединение с зарубежным сервером.
  • Раздельная аутентификация: пароль «клиент → Entry Node» знают только конечные пользователи. Пароль «Entry Node → Exit Node» знают только серверы. Компрометация клиентского пароля не раскрывает пароль соединения между серверами.
  • Анонимизация выхода: Exit Node передаёт трафик через Cloudflare WARP. Конечные сайты видят IP Cloudflare, а не IP ваших серверов.
  • Параллельные независимые каналы: Hysteria2 работает рядом с NaïveProxy и 3x-ui на тех же серверах, не конфликтуя с ними. Падение одного канала не блокирует другие.

Схема конкретного решения

Компоненты инфраструктуры

Компонент Роль Адрес / Домен ОС
Клиентские устройства Конечные пользователи — iOS, Android, Windows, macOS, Linux
OpenWrt роутер Прозрачный обход для всей домашней сети — OpenWrt 24.10 (podkop нативно поддерживает hy2 URI)
RU сервер (Entry Node) Точка входа клиентов YOUR_RU_SERVER_IP / your-hy2-ru-domain.example.com Ubuntu 24.04
EU сервер (Exit Node) Точка выхода в интернет YOUR_EU_SERVER_IP / your-hy2-eu-domain.example.com Ubuntu 24.04
Cloudflare WARP Финальный выход в интернет IP Cloudflare —

Полная схема прохождения трафика

Схема трафика Hysteria2: Клиенты → RU Entry Node → EU Exit Node → WARP → Интернет
Схема трафика Hysteria2: Клиенты → RU Entry Node → EU Exit Node → WARP → Интернет

Протоколы на каждом участке цепочки

Участок Протокол Шифрование Что видит наблюдатель
Устройство → Роутер Обычный TCP/UDP Нет (локальная сеть) Локальный трафик
Роутер / ПК → RU сервер QUIC / UDP (Hysteria2) TLS 1.3 (Let's Encrypt) HTTPS/HTTP3 трафик к обычному сайту
RU клиент → EU сервер QUIC / UDP (Hysteria2) TLS 1.3 (Let's Encrypt) HTTPS/HTTP3 трафик к обычному сайту
EU сервер → 3x-ui inbound SOCKS5 (localhost) — (только на EU сервере) Только локально
EU 3x-ui → WARP WireGuard (gVisor TUN) WireGuard Зашифрованный WireGuard к Cloudflare
WARP → Интернет Обычный TCP/UDP — IP Cloudflare

Дополнительные каналы (параллельно Hysteria2)

Hysteria2 работает независимо от NaïveProxy и 3x-ui. Все три канала активны одновременно:

Канал Протокол Маршрут Статус
3x-ui каскад (основной) VLESS / REALITY Клиенты → RU 3x-ui → EU 3x-ui → Интернет ✅ Активен
NaïveProxy каскад HTTP/2 naïve Роутер/ПК → RU Caddy → EU Caddy → WARP ✅ Активен
Hysteria2 каскад (этот гайд) QUIC/UDP hy2 Клиенты → RU hy2 → EU hy2 → WARP ✅ Активен
MTProto (Telegram) MTProto Клиенты Telegram → EU :8443, :2443 ✅ Активен

Предварительные требования

Инфраструктура

Компонент Требования
EU сервер Ubuntu 24.04, публичный IP, домен с A-записью → IP сервера
RU сервер Ubuntu 24.04, публичный IP, домен с A-записью → IP сервера
3x-ui на EU сервере Уже установлен и работает; настроен outbound WARP
Docker Установлен на обоих серверах
Порт 2053 UDP Должен быть свободен на обоих серверах
TLS-сертификат Получается через Caddy (caddy-naive), либо через certbot — описано в разделе ниже

DNS-записи (создать заранее, до установки)

Let's Encrypt проверяет доступность домена по HTTP (порт 80) перед выдачей сертификата. DNS-записи должны быть созданы и распространены (propagated) до первого запуска Caddy.

Запись Тип Значение Примечание
your-hy2-ru-domain.example.com A IP RU сервера Используется Hysteria2 сервером на RU
your-hy2-eu-domain.example.com A IP EU сервера Используется Hysteria2 сервером на EU

Примечание: если NaïveProxy (Caddy) уже настроен на тех же серверах с теми же доменами, отдельные DNS-записи для Hysteria2 не нужны — используется тот же домен и тот же сертификат.

Используемые заглушки

В данном руководстве применяются следующие заглушки. Замени их реальными значениями перед использованием:

Заглушка Описание
YOUR_RU_SERVER_IP Публичный IP RU сервера
YOUR_EU_SERVER_IP Публичный IP EU сервера
your-hy2-ru-domain.example.com Домен для RU сервера (для TLS-сертификата)
your-hy2-eu-domain.example.com Домен для EU сервера (для TLS-сертификата)
YOUR_EMAIL@example.com Email для Let's Encrypt (уведомления об истечении сертификата)
EU_HY2_PASSWORD Пароль соединения RU клиент → EU сервер (генерируется один раз)

Для генерации пароля EU_HY2_PASSWORD используй:

openssl rand -base64 18

Сохрани результат — он понадобится в конфигах обоих серверов.


Часть 1: EU сервер (Exit Node)

Цель: развернуть Hysteria2 сервер в Docker, который принимает соединения от RU клиента и перенаправляет трафик через 3x-ui → Cloudflare WARP.

Все команды в этом разделе выполняются на EU сервере под root.

1.1 Установка Docker

Если Docker уже установлен (проверить: docker --version) — пропусти этот шаг.

# Обновить пакеты
apt-get update && apt-get upgrade -y

# Установить зависимости
apt-get install -y ca-certificates curl gnupg lsb-release

# Добавить GPG-ключ Docker
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

# Добавить репозиторий Docker
echo "deb [arch=$(dpkg --print-architecture) \
  signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  > /etc/apt/sources.list.d/docker.list

# Установить Docker Engine
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Включить автозапуск
systemctl enable docker
systemctl start docker

# Проверить
docker --version
docker compose version

Ожидаемый вывод:

Docker version 27.x.x, build ...
Docker Compose version v2.x.x

1.2 Настройка 3x-ui: создание inbound hy2-out → WARP

Hysteria2 Exit Node направляет трафик через 3x-ui SOCKS5 inbound, который в свою очередь маршрутизирует его в Cloudflare WARP. Этот шаг выполняется в веб-интерфейсе 3x-ui до запуска Hysteria2.

Условие: в 3x-ui должен быть настроен outbound WARP (WireGuard/gVisor).

1.2.1 Создать новый inbound в 3x-ui

В веб-интерфейсе 3x-ui (EU сервер) перейди в Inbounds → Add Inbound и заполни следующие поля:

Параметр Значение Пояснение
Примечание (Remark) hy2-out Произвольное имя для идентификации в правилах маршрутизации
Протокол mixed Поддерживает SOCKS5 и HTTP одновременно
Listen IP 127.0.0.1 Только localhost — недоступен извне
Порт 24364 grep 24364
Аутентификация Выключена Hysteria2 сам отвечает за авторизацию клиентов
UDP Включён Требуется для DNS-запросов через туннель

Нажми Save — inbound немедленно активируется.

1.2.2 Создать правило маршрутизации

В 3x-ui перейди в Routing → Add Rule (или Settings → Routing Rules):

Параметр Значение
Inbound Tag hy2-out (тег созданного выше inbound)
Outbound Tag warp (или название твоего WARP outbound в 3x-ui)

Сохрани и перезапусти Xray внутри 3x-ui.

1.2.3 Проверить, что порт 24364 слушает

ss -tlnp | grep 24364

Ожидаемый вывод:

LISTEN  0  128  127.0.0.1:24364  0.0.0.0:*  users:(("xray",...))

1.3 TLS-сертификат для Hysteria2

Hysteria2 требует действующий TLS-сертификат (самоподписанный не рекомендуется — клиентам потребуется флаг insecure: true). В нашей схеме используется Let's Encrypt сертификат, получаемый и автоматически обновляемый через Caddy (caddy-naive стек).

Caddy хранит сертификаты в именованном Docker volume caddy-naive_caddy_data, и Hysteria2 подключает их через bind mount (read-only) — никаких лишних сервисов, никакого дублирования.

Вариант А: Caddy (caddy-naive) уже установлен — рекомендуется

Если NaïveProxy (caddy-naive) уже настроен и работает, сертификат уже получен. Проверь:

# Найти путь к сертификату в Docker volume
DOMAIN="your-hy2-eu-domain.example.com"
CERT_DIR="/var/lib/docker/volumes/caddy-naive_caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}"

ls -la "${CERT_DIR}/"

Ожидаемый вывод — два файла:

your-hy2-eu-domain.example.com.crt
your-hy2-eu-domain.example.com.key

Если файлы есть — переходи к разделу 1.4. Если volume или папка не существуют — сначала установи caddy-naive (NaïveProxy) или воспользуйся Вариантом Б.

Вариант Б: получить сертификат через certbot (если Caddy не установлен)

Этот вариант используется, если caddy-naive не установлен и на серверах нет другого HTTP-сервера на портах 80/443.

# Установить certbot
apt-get install -y certbot

# Получить сертификат (standalone — Certbot запустит временный HTTP-сервер на :80)
certbot certonly --standalone \
  -d your-hy2-eu-domain.example.com \
  --email YOUR_EMAIL@example.com \
  --agree-tos --non-interactive

# Проверить
ls /etc/letsencrypt/live/your-hy2-eu-domain.example.com/

Сертификаты будут по пути /etc/letsencrypt/live/your-hy2-eu-domain.example.com/. В этом случае в docker-compose.yml (раздел 1.6) замени volume на:

- /etc/letsencrypt/live/your-hy2-eu-domain.example.com:/etc/hysteria/certs:ro
- /etc/letsencrypt/archive/your-hy2-eu-domain.example.com:/etc/letsencrypt/archive/your-hy2-eu-domain.example.com:ro

И в server.yaml пути к файлам:

tls:
  cert: /etc/hysteria/certs/fullchain.pem
  key: /etc/hysteria/certs/privkey.pem

1.4 Создание структуры проекта

mkdir -p /opt/hysteria-eu
cd /opt/hysteria-eu

Итоговая структура:

/opt/hysteria-eu/
├── docker-compose.yml
└── server.yaml

1.5 Конфигурация сервера: server.yaml

Создай файл конфигурации. Замени EU_HY2_PASSWORD сгенерированным паролем:

cat > /opt/hysteria-eu/server.yaml << 'EOF'
listen: :2053

tls:
  cert: /etc/hysteria/certs/your-hy2-eu-domain.example.com.crt
  key: /etc/hysteria/certs/your-hy2-eu-domain.example.com.key

auth:
  type: password
  password: EU_HY2_PASSWORD

ignoreClientBandwidth: true

masquerade:
  type: proxy
  proxy:
    url: https://news.ycombinator.com/
    rewriteHost: true

outbounds:
  - name: warp
    type: socks5
    socks5:
      addr: 127.0.0.1:24364

acl:
  inline:
    - "warp(all)"
EOF

Описание ключевых параметров:

Параметр Значение Пояснение
listen: :2053 UDP порт 2053 Hysteria2 слушает на всех интерфейсах
tls.cert / tls.key Пути к сертификату Берутся из caddy-naive Docker volume (bind mount)
auth.type: password Одиночный пароль Для соединения RU клиент → EU сервер (клиентские устройства этот пароль не знают)
ignoreClientBandwidth: true Игнорировать лимит клиента Серверная сторона управляет congestion control самостоятельно
masquerade.type: proxy Проксировать реальный сайт Неавторизованные запросы получают реальный контент ycombinator.com — сервер выглядит как обычный HTTPS-сайт
outbounds.warp SOCKS5 на 127.0.0.1:24364 Исходящий трафик → 3x-ui inbound "hy2-out" → WARP
acl: warp(all) Направить весь трафик в warp Синтаксис: имя outbound в скобках, адрес/маска после

1.6 Конфигурация Docker Compose: docker-compose.yml

cat > /opt/hysteria-eu/docker-compose.yml << 'EOF'
services:
  hysteria-eu:
    image: tobyxdd/hysteria:latest
    container_name: hysteria-eu
    restart: unless-stopped
    network_mode: host
    command: server --config /etc/hysteria/server.yaml
    volumes:
      - ./server.yaml:/etc/hysteria/server.yaml:ro
      - /var/lib/docker/volumes/caddy-naive_caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/your-hy2-eu-domain.example.com:/etc/hysteria/certs:ro
EOF

Важные моменты:

  • network_mode: host — обязателен, чтобы контейнер видел 127.0.0.1:24364 (3x-ui inbound).
  • :ro — сертификаты подключены только для чтения, Hysteria2 их не изменяет.
  • Путь к сертификату — bind mount из Docker volume caddy-naive. Если используется certbot (Вариант Б) — замени путь согласно инструкции раздела 1.3.

1.7 Первый запуск

cd /opt/hysteria-eu

# Скачать образ и запустить
docker compose pull
docker compose up -d

# Подождать 3 секунды и проверить логи
sleep 3 && docker compose logs

Ожидаемый вывод в логах:

INFO  server mode
INFO  server up and running   {"listen": ":2053"}

Если в логах ошибка — см. раздел «Диагностика проблем» (Часть 5).

1.8 Проверка: порт и соединение

# Проверить, что Hysteria2 слушает на UDP :2053
ss -ulnp | grep ':2053'

Ожидаемый вывод:

UNCONN  0  0  0.0.0.0:2053  0.0.0.0:*  users:(("hysteria",...))
# Статус контейнера
docker compose ps
NAME          IMAGE                     STATUS
hysteria-eu   tobyxdd/hysteria:latest   Up X minutes

Часть 2: RU сервер (Entry Node)

Цель: развернуть два Docker-контейнера:

  • hysteria-ru-server — принимает соединения от клиентов, аутентифицирует по users.txt.
  • hysteria-ru-client — подключается к EU серверу, слушает SOCKS5 на 127.0.0.1:10810.

Между ними: hysteria-ru-server направляет трафик через SOCKS5 → hysteria-ru-client → EU.

Все команды в этом разделе выполняются на RU сервере под root.

2.1 Установка Docker

Аналогично EU серверу (раздел 1.1). Если Docker уже установлен — пропусти.

apt-get update && apt-get upgrade -y
apt-get install -y ca-certificates curl gnupg lsb-release

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) \
  signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  > /etc/apt/sources.list.d/docker.list

apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
systemctl enable docker && systemctl start docker

docker --version && docker compose version

2.2 TLS-сертификат для RU сервера

Аналогично EU серверу. На RU сервере также используется сертификат от Caddy (caddy-naive), если NaïveProxy настроен.

Вариант А: через Caddy (caddy-naive) — рекомендуется

DOMAIN="your-hy2-ru-domain.example.com"
CERT_DIR="/var/lib/docker/volumes/caddy-naive_caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}"

ls -la "${CERT_DIR}/"

Если файлы .crt и .key присутствуют — всё готово, переходи к разделу 2.3.

Вариант Б: через certbot (если Caddy не установлен)

apt-get install -y certbot

certbot certonly --standalone \
  -d your-hy2-ru-domain.example.com \
  --email YOUR_EMAIL@example.com \
  --agree-tos --non-interactive

ls /etc/letsencrypt/live/your-hy2-ru-domain.example.com/

2.3 Создание структуры проекта

mkdir -p /opt/hysteria-ru/users
cd /opt/hysteria-ru

Итоговая структура:

/opt/hysteria-ru/
├── docker-compose.yml
├── server.yaml
├── client.yaml
└── users/
    ├── users.txt        ← список пользователей (username password)
    └── check-auth.sh    ← скрипт авторизации, вызывается при каждом подключении

2.4 Система аутентификации пользователей

RU сервер использует auth.type: command — при каждом входящем подключении Hysteria2 вызывает внешний скрипт и передаёт ему пароль клиента в аргументах. Скрипт ищет пароль в файле users.txt: если совпадение найдено — выводит имя пользователя (оно попадает в логи) и завершается с кодом 0. Если нет — завершается с кодом 1 (отказ в подключении).

Преимущество этого подхода: users.txt можно редактировать в любой момент без перезапуска контейнера. Изменения применяются немедленно.

2.4.1 Файл пользователей: users/users.txt

Формат: одна строка на пользователя — имя пользователя и пароль, разделённые пробелом.

# Сгенерировать первого пользователя (пример: "default")
PASS=$(openssl rand -base64 18)
echo "default $PASS" > /opt/hysteria-ru/users/users.txt
echo "Пароль пользователя default: $PASS"

Формат файла:

username1 пароль1
username2 пароль2

Важно: не используй пробелы внутри пароля — пароль считывается по второму полю строки.

2.4.2 Скрипт авторизации: users/check-auth.sh

Hysteria2 вызывает скрипт с тремя аргументами: $1 — адрес клиента, $2 — пароль, $3 — tx bandwidth.

cat > /opt/hysteria-ru/users/check-auth.sh << 'EOF'
#!/bin/sh
AUTH_PAYLOAD="$2"
USERS_FILE="/etc/hysteria/users/users.txt"

USERNAME=$(awk -v pwd="$AUTH_PAYLOAD" '$2 == pwd {print $1; exit}' "$USERS_FILE" 2>/dev/null)

if [ -n "$USERNAME" ]; then
    echo "$USERNAME"
    exit 0
fi
exit 1
EOF

chmod +x /opt/hysteria-ru/users/check-auth.sh

Почему используется awk, а не grep:

  • Hysteria2 использует Docker-образ на базе Alpine Linux с BusyBox.
  • BusyBox grep не поддерживает флаг -P (Perl-совместимые регулярки).
  • awk доступен во всех Unix/Linux окружениях и работает стабильно.

2.5 Конфигурация сервера: server.yaml

cat > /opt/hysteria-ru/server.yaml << 'EOF'
listen: :2053

tls:
  cert: /etc/hysteria/certs/your-hy2-ru-domain.example.com.crt
  key: /etc/hysteria/certs/your-hy2-ru-domain.example.com.key

auth:
  type: command
  command: /etc/hysteria/users/check-auth.sh

ignoreClientBandwidth: true

masquerade:
  type: proxy
  proxy:
    url: https://news.ycombinator.com/
    rewriteHost: true

outbounds:
  - name: chain
    type: socks5
    socks5:
      addr: 127.0.0.1:10810

acl:
  inline:
    - "chain(all)"
EOF

Описание ключевых параметров:

Параметр Значение Пояснение
auth.type: command Внешний скрипт При каждом подключении вызывается check-auth.sh
auth.command Путь внутри контейнера Скрипт смонтирован через volume: ./users:/etc/hysteria/users
outbounds.chain SOCKS5 на 127.0.0.1:10810 Трафик → hysteria-ru-client → EU
acl: chain(all) Весь трафик через chain Синтаксис: имя outbound, затем адрес в скобках

2.6 Конфигурация клиента: client.yaml

RU клиент подключается к EU серверу и поднимает SOCKS5 на 127.0.0.1:10810.

cat > /opt/hysteria-ru/client.yaml << 'EOF'
server: your-hy2-eu-domain.example.com:2053

auth: EU_HY2_PASSWORD

socks5:
  listen: 127.0.0.1:10810
EOF

Замени EU_HY2_PASSWORD на тот же пароль, который указан в server.yaml EU сервера.

2.7 Конфигурация Docker Compose: docker-compose.yml

cat > /opt/hysteria-ru/docker-compose.yml << 'EOF'
services:
  hysteria-ru-server:
    image: tobyxdd/hysteria:latest
    container_name: hysteria-ru-server
    restart: unless-stopped
    network_mode: host
    command: server --config /etc/hysteria/server.yaml
    volumes:
      - ./server.yaml:/etc/hysteria/server.yaml:ro
      - ./users:/etc/hysteria/users:ro
      - /var/lib/docker/volumes/caddy-naive_caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/your-hy2-ru-domain.example.com:/etc/hysteria/certs:ro

  hysteria-ru-client:
    image: tobyxdd/hysteria:latest
    container_name: hysteria-ru-client
    restart: unless-stopped
    network_mode: host
    command: client --config /etc/hysteria/client.yaml
    volumes:
      - ./client.yaml:/etc/hysteria/client.yaml:ro
EOF

Критически важные моменты:

  • Директория ./users монтируется в оба контейнера... нет, только в hysteria-ru-server. Клиентский контейнер видит только свой client.yaml.
  • При изменении docker-compose.yml (добавлении volume) нужен пересоздать контейнер: docker compose down && docker compose up -d. Простой restart не применяет изменения volumes.

2.8 Первый запуск

cd /opt/hysteria-ru

# Скачать образ
docker compose pull

# Запустить оба контейнера
docker compose up -d

# Подождать и проверить логи
sleep 3 && docker compose logs

Ожидаемый вывод:

hysteria-ru-server  | INFO  server mode
hysteria-ru-server  | INFO  server up and running  {"listen": ":2053"}
hysteria-ru-client  | INFO  client mode
hysteria-ru-client  | INFO  connected to server  {"udpEnabled": true, "count": 1}
hysteria-ru-client  | INFO  SOCKS5 server listening  {"addr": "127.0.0.1:10810"}

2.9 Проверка цепочки с RU сервера

Тест: запустить временный Hysteria2 клиент на RU сервере и проверить, что трафик выходит через WARP (IP Cloudflare):

# Создать временный конфиг тест-клиента
cat > /tmp/hy2-test.yaml << EOF
server: 127.0.0.1:2053
auth: $(awk 'NR==1{print $2}' /opt/hysteria-ru/users/users.txt)
tls:
  insecure: true
  sni: your-hy2-ru-domain.example.com
socks5:
  listen: 127.0.0.1:11080
EOF

# Запустить тест-клиент
docker run -d --name hy2-test --network host \
  -v /tmp/hy2-test.yaml:/etc/hysteria/client.yaml:ro \
  tobyxdd/hysteria:latest client --config /etc/hysteria/client.yaml

# Ждать 3 секунды
sleep 3

# Проверить IP выхода — должен быть Cloudflare WARP IP
echo "IP через цепочку:"
curl -s --max-time 15 -x socks5h://127.0.0.1:11080 https://ifconfig.me

# Очистить
docker rm -f hy2-test
rm -f /tmp/hy2-test.yaml

Если IP вывода принадлежит Cloudflare (начинается с 2a09: для IPv6 или относится к диапазонам WARP) — цепочка RU → EU → WARP работает корректно.

Примечание: параметр insecure: true и sni нужны в тест-клиенте, потому что он подключается по IP (127.0.0.1), а сертификат выдан на домен. Реальные клиенты подключаются по домену и такие параметры не требуются.


Часть 3: Управление пользователями (hy2-users.sh)

3.1 Что это и зачем

Для управления пользователями Hysteria2 на RU сервере предназначен скрипт hy2-users.sh. Он предоставляет интерактивное меню с набором операций над файлом users.txt.

Скрипт решает следующие задачи:

Операция Описание
Список пользователей Показывает всех пользователей из users.txt с маскированными паролями
Добавить пользователя Запрашивает имя, автоматически генерирует криптостойкий пароль, сохраняет в users.txt, показывает готовый Hysteria2 URI
Удалить пользователя Интерактивный выбор из списка, удаление строки из users.txt
Показать конфиг Генерирует и показывает Hysteria2 URI для выбранного пользователя

Ключевые особенности:

  • Нет перезапуска контейнера. Hysteria2 с auth.type: command читает файл при каждом подключении — изменения в users.txt применяются мгновенно.
  • Пароли генерируются автоматически через openssl rand — случайные, криптостойкие.
  • URI формат соответствует стандарту Hysteria2 и принимается всеми клиентами: NekoBox, husi, Exclave, podkop.

3.2 Установка скрипта

Скрипт хранится в репозитории по пути scripts/hy2-users.sh. Установка на сервер:

# Скопировать скрипт на сервер
scp scripts/hy2-users.sh root@YOUR_RU_SERVER_IP:/usr/local/bin/hy2-users.sh

# Сделать исполняемым
ssh root@YOUR_RU_SERVER_IP "chmod +x /usr/local/bin/hy2-users.sh"

Или выполнить непосредственно на RU сервере (если скрипт уже там):

chmod +x /usr/local/bin/hy2-users.sh

Проверка переменных в начале скрипта. Открой скрипт и убедись, что следующие переменные указывают на правильные значения:

head -15 /usr/local/bin/hy2-users.sh

Должны быть:

  • USERS_FILE="/opt/hysteria-ru/users/users.txt"
  • DOMAIN="your-hy2-ru-domain.example.com"
  • PORT="2053"

Если домен указан неверно — исправь:

sed -i 's|DOMAIN=.*|DOMAIN="your-hy2-ru-domain.example.com"|' /usr/local/bin/hy2-users.sh

3.3 Использование

sudo bash /usr/local/bin/hy2-users.sh

Скрипт показывает меню:

┌─────────────────────────────────────────────┐
│  Hysteria2 — управление пользователями RU   │
└─────────────────────────────────────────────┘

  1. Список пользователей
  2. Добавить пользователя
  3. Удалить пользователя
  4. Показать конфиг клиента

  0. Выход

Выбери действие:

При добавлении нового пользователя скрипт выводит готовый URI:

hysteria2://СГЕНЕРИРОВАННЫЙ_ПАРОЛЬ@your-hy2-ru-domain.example.com:2053

Этот URI вставляется напрямую в клиентское приложение (см. Часть 4).

3.4 Исходный код скрипта

▶ Показать исходный код hy2-users.sh
#!/bin/bash
# =============================================================
# hy2-users — управление пользователями Hysteria2 на RU сервере
# users.txt: /opt/hysteria-ru/users/users.txt
# Формат: username password (по одной строке)
# Перезапуск контейнера НЕ требуется — изменения применяются мгновенно
# =============================================================

USERS_FILE="/opt/hysteria-ru/users/users.txt"
DOMAIN="your-hy2-ru-domain.example.com"
PORT="2053"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

# --- Вспомогательные функции ---

check_root() {
    if [[ $EUID -ne 0 ]]; then
        echo -e "${RED}Ошибка: запусти скрипт от root (sudo bash hy2-users.sh)${NC}"
        exit 1
    fi
}

check_users_file() {
    if [[ ! -f "$USERS_FILE" ]]; then
        echo -e "${RED}Ошибка: файл пользователей не найден: $USERS_FILE${NC}"
        exit 1
    fi
}

get_usernames() {
    awk '{print $1}' "$USERS_FILE"
}

get_password() {
    local user="$1"
    awk -v u="$user" '$1 == u {print $2}' "$USERS_FILE"
}

press_enter() {
    echo ""
    read -rp "Нажми Enter для возврата в меню..."
}

print_config_for() {
    local user="$1"
    local pass="$2"
    echo ""
    echo -e "${BOLD}┌─────────────────────────────────────────────┐${NC}"
    echo -e "${BOLD}│  Hysteria2 URI для клиента                  │${NC}"
    echo -e "${BOLD}└─────────────────────────────────────────────┘${NC}"
    echo ""
    echo -e "${BOLD}  hysteria2://${pass}@${DOMAIN}:${PORT}${NC}"
    echo ""
    echo -e "${CYAN}Для podkop (OpenWrt):${NC}  вставить URI как hy2 outbound"
    echo -e "${CYAN}Для мобильных клиентов:${NC}  NekoBox, husi, Exclave — импорт URI"
    echo -e "${CYAN}Пользователь:${NC}  ${user}"
}

# --- Функции меню ---

list_users() {
    echo ""
    echo -e "${BOLD}${CYAN}=== Список пользователей ===${NC}"
    mapfile -t users < <(get_usernames)
    if [[ ${#users[@]} -eq 0 ]]; then
        echo -e "${YELLOW}Пользователей нет.${NC}"
    else
        for i in "${!users[@]}"; do
            echo -e "  ${BOLD}$((i+1)).${NC} ${users[$i]}"
        done
        echo ""
        echo -e "  Всего: ${#users[@]} пользователь(ей)"
    fi
    press_enter
}

add_user() {
    echo ""
    echo -e "${BOLD}${CYAN}=== Добавить пользователя ===${NC}"

    read -rp "Имя пользователя: " username
    username=$(echo "$username" | tr -d ' ')

    if [[ -z "$username" ]]; then
        echo -e "${RED}Имя не может быть пустым.${NC}"
        press_enter
        return
    fi

    if grep -q "^${username} " "$USERS_FILE" 2>/dev/null; then
        echo -e "${RED}Пользователь '${username}' уже существует.${NC}"
        press_enter
        return
    fi

    read -rp "Пароль (Enter = сгенерировать автоматически): " password
    if [[ -z "$password" ]]; then
        password=$(openssl rand -base64 18 | tr -d '=/+' | head -c 22)
        echo -e "  Сгенерирован пароль: ${BOLD}${password}${NC}"
    fi

    echo "${username} ${password}" >> "$USERS_FILE"
    echo -e "${GREEN}Пользователь '${username}' добавлен.${NC}"
    echo -e "${YELLOW}Изменения применяются мгновенно — перезапуск не нужен.${NC}"

    print_config_for "$username" "$password"
    press_enter
}

delete_user() {
    echo ""
    echo -e "${BOLD}${CYAN}=== Удалить пользователя ===${NC}"
    mapfile -t users < <(get_usernames)

    if [[ ${#users[@]} -eq 0 ]]; then
        echo -e "${YELLOW}Нет пользователей для удаления.${NC}"
        press_enter
        return
    fi

    for i in "${!users[@]}"; do
        echo -e "  ${BOLD}$((i+1)).${NC} ${users[$i]}"
    done
    echo "  0. Отмена"
    echo ""
    read -rp "Выбери номер для удаления: " choice

    if [[ "$choice" == "0" || -z "$choice" ]]; then
        return
    fi

    if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#users[@]} )); then
        echo -e "${RED}Неверный номер.${NC}"
        press_enter
        return
    fi

    target="${users[$((choice-1))]}"
    read -rp "Удалить пользователя '${target}'? (y/N): " confirm
    if [[ "$confirm" =~ ^[Yy]$ ]]; then
        sed -i "/^${target} /d" "$USERS_FILE"
        echo -e "${GREEN}Пользователь '${target}' удалён.${NC}"
        echo -e "${YELLOW}Изменения применяются мгновенно — перезапуск не нужен.${NC}"
    else
        echo "Отменено."
    fi
    press_enter
}

show_config() {
    echo ""
    echo -e "${BOLD}${CYAN}=== URI клиента ===${NC}"
    mapfile -t users < <(get_usernames)

    if [[ ${#users[@]} -eq 0 ]]; then
        echo -e "${YELLOW}Нет пользователей.${NC}"
        press_enter
        return
    fi

    for i in "${!users[@]}"; do
        echo -e "  ${BOLD}$((i+1)).${NC} ${users[$i]}"
    done
    echo "  0. Отмена"
    echo ""
    read -rp "Выбери номер пользователя: " choice

    if [[ "$choice" == "0" || -z "$choice" ]]; then
        return
    fi

    if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#users[@]} )); then
        echo -e "${RED}Неверный номер.${NC}"
        press_enter
        return
    fi

    local user="${users[$((choice-1))]}"
    local pass
    pass=$(get_password "$user")
    print_config_for "$user" "$pass"
    press_enter
}

# --- Главное меню ---

main_menu() {
    check_root
    check_users_file

    while true; do
        clear
        echo -e "${BOLD}${CYAN}"
        echo "╔══════════════════════════════════════╗"
        echo "║   Hysteria2 User Manager (RU)        ║"
        echo "║   Домен: ${DOMAIN}  ║"
        echo "╚══════════════════════════════════════╝"
        echo -e "${NC}"

        mapfile -t users < <(get_usernames)
        echo -e "  Активных пользователей: ${BOLD}${#users[@]}${NC}"
        echo ""
        echo -e "  ${BOLD}1.${NC} Список пользователей"
        echo -e "  ${BOLD}2.${NC} Добавить пользователя"
        echo -e "  ${BOLD}3.${NC} Удалить пользователя"
        echo -e "  ${BOLD}4.${NC} Показать URI клиента"
        echo -e "  ${BOLD}0.${NC} Выход"
        echo ""
        read -rp "Выбор: " option

        case "$option" in
            1) list_users ;;
            2) add_user ;;
            3) delete_user ;;
            4) show_config ;;
            0) echo "Выход."; exit 0 ;;
            *) echo -e "${RED}Неверный выбор.${NC}"; sleep 1 ;;
        esac
    done
}

main_menu

Часть 4: Настройка клиентов

4.1 Формат URI Hysteria2

Hysteria2 использует стандартизированный URI для передачи параметров подключения клиентам:

hysteria2://ПАРОЛЬ@ДОМЕН:ПОРТ

Пример:

hysteria2://aBcDeFgHiJkLmNoPqRsT@your-hy2-ru-domain.example.com:2053

URI можно получить через скрипт hy2-users.sh (пункт 4 меню — «Показать конфиг клиента»).

4.2 Мобильные клиенты (iOS / Android)

Рекомендуемые приложения с нативной поддержкой Hysteria2:

Приложение Платформа Как импортировать URI
NekoBox Android Главный экран → + → Add → Hysteria2 → вставить URI
husi Android Профили → + → Hysteria2 → вставить URI
Exclave iOS Конфигурации → + → Hysteria2 → вставить URI
Shadowrocket iOS Главная → + → Тип: Hysteria2 → вставить URI

Все эти приложения принимают URI в формате hysteria2://... напрямую — через кнопку импорта, QR-код или буфер обмена.

4.3 ПК-клиенты (Windows / macOS / Linux)

Вариант А: официальный бинарь Hysteria2

Скачать бинарь с официального репозитория: https://github.com/apernet/hysteria/releases

Создать файл конфигурации config.yaml:

server: your-hy2-ru-domain.example.com:2053

auth: ВАШ_ПАРОЛЬ

socks5:
  listen: 127.0.0.1:1080

http:
  listen: 127.0.0.1:8080

Запустить:

# Linux / macOS
./hysteria client --config config.yaml

# Windows (PowerShell)
.\hysteria.exe client --config config.yaml

После запуска:

  • SOCKS5 прокси доступен на 127.0.0.1:1080
  • HTTP прокси доступен на 127.0.0.1:8080

Вариант Б: через NekoRay / Hiddify Next (Windows / Linux)

Приложения NekoRay и Hiddify Next принимают Hysteria2 URI — импорт через меню или буфер обмена. Автоматически управляют запуском клиентского процесса.

4.4 OpenWrt / podkop

podkop (реализация на базе sing-box) нативно поддерживает Hysteria2 URI. Никакой дополнительной установки бинарей не требуется.

В интерфейсе podkop (LuCI → Network → podkop → Outbound):

  • Тип: Hysteria2
  • URI: hysteria2://ПАРОЛЬ@your-hy2-ru-domain.example.com:2053

Или через UCI:

uci set podkop.main.proxy='hysteria2://ПАРОЛЬ@your-hy2-ru-domain.example.com:2053'
uci commit podkop
/etc/init.d/podkop restart

После сохранения все устройства домашней сети автоматически получают обход без дополнительной настройки.


Часть 5: Проверка всей цепочки

5.1 Тест RU сервера: цепочка RU → EU → WARP

Выполнить на RU сервере — проверяет, что трафик выходит через WARP:

# SOCKS5 порт 10810 — это точка выхода RU клиента (= вход в EU сервер → WARP)
curl -s --max-time 20 -x socks5h://127.0.0.1:10810 https://ifconfig.me
echo ""

Результат должен быть IP Cloudflare WARP — например, начинается с 2a09: (IPv6) или принадлежит AS13335 (Cloudflare).

Проверить AS:

IP=$(curl -s --max-time 20 -x socks5h://127.0.0.1:10810 https://ifconfig.me)
curl -s "https://ipinfo.io/${IP}/org"
echo ""

Ожидаемый вывод: строка содержит Cloudflare.

5.2 Тест с реального клиента

После добавления пользователя через hy2-users.sh и получения URI:

  1. Вставь URI в клиентское приложение (NekoBox, husi, Shadowrocket).
  2. Активируй профиль.
  3. Открой https://ifconfig.me в браузере — должен отображаться IP Cloudflare WARP.
  4. Открой https://www.whatismybrowser.com/detect/what-is-my-ip-address для проверки отсутствия WebRTC утечки.

5.3 Диагностика проблем

Проблема: hysteria-eu не запускается — ошибка TLS

docker compose -f /opt/hysteria-eu/docker-compose.yml logs hysteria-eu

Возможные причины:

  • Путь к сертификату неверен → проверь ls по пути, указанному в volumes.
  • Сертификат ещё не был получен Caddy → проверь, что caddy-naive запущен и прошёл ACME-проверку.
  • Caddy хранит сертификат под другим именем → выполни find /var/lib/docker/volumes/caddy-naive_caddy_data/_data -name "*.crt".

Проблема: hysteria-ru-server выдаёт auth error

docker compose -f /opt/hysteria-ru/docker-compose.yml logs hysteria-ru-server

Проверить вручную (на RU сервере):

# Проверить, что скрипт находится на месте внутри контейнера
docker exec hysteria-ru-server ls -la /etc/hysteria/users/

# Запустить скрипт вручную с тестовым паролем
docker exec hysteria-ru-server /etc/hysteria/users/check-auth.sh addr ТЕСТОВЫЙ_ПАРОЛЬ 0
echo "Exit code: $?"

Если Exit code: 0 — скрипт работает. Если 1 — пароль не найден в users.txt.

Проблема: hysteria-ru-client не подключается к EU

docker compose -f /opt/hysteria-ru/docker-compose.yml logs hysteria-ru-client

Возможные причины:

  • EU_HY2_PASSWORD в client.yaml не совпадает с паролем в EU server.yaml.
  • UDP порт 2053 заблокирован на EU сервере (firewall) → проверь ss -ulnp | grep 2053 на EU сервере.
  • EU сервер ещё не запущен.

Проблема: ACL ошибка при запуске

FATAL failed to load server config: invalid config: acl.inline: invalid syntax at line 1

Неверный синтаксис ACL. Правильный формат: "outbound_name(address)":

  • ✅ "warp(all)" — направить всё в outbound "warp"
  • ✅ "chain(all)" — направить всё в outbound "chain"
  • ❌ "outbound(warp) all" — неверно

Часть 6: Обслуживание

6.1 Управление контейнерами

# EU сервер
cd /opt/hysteria-eu
docker compose up -d         # запустить
docker compose down          # остановить
docker compose restart       # перезапустить
docker compose logs -f       # следить за логами в реальном времени
docker compose ps            # статус

# RU сервер (оба контейнера)
cd /opt/hysteria-ru
docker compose up -d
docker compose down
docker compose restart
docker compose logs -f

# RU сервер (по отдельности)
docker compose restart hysteria-ru-server   # только сервер
docker compose restart hysteria-ru-client   # только клиент
docker compose logs -f hysteria-ru-server   # логи только сервера
docker compose logs -f hysteria-ru-client   # логи только клиента

6.2 Обновление сертификатов

TLS-сертификаты обновляются Caddy автоматически (за 30 дней до истечения). После автообновления Hysteria2 начнёт использовать новый сертификат при следующем перезапуске.

Для принудительного применения нового сертификата:

# EU сервер
cd /opt/hysteria-eu && docker compose restart

# RU сервер
cd /opt/hysteria-ru && docker compose restart hysteria-ru-server

Проверить дату истечения текущего сертификата:

# EU сервер
DOMAIN="your-hy2-eu-domain.example.com"
CERT="/var/lib/docker/volumes/caddy-naive_caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}/${DOMAIN}.crt"
openssl x509 -in "${CERT}" -noout -enddate

6.3 Обновление Docker-образа

Hysteria2 активно развивается. Для обновления до последней версии:

# EU сервер
cd /opt/hysteria-eu
docker compose pull           # скачать новый образ
docker compose up -d          # перезапустить с новым образом
docker compose logs --tail=5  # проверить старт

# RU сервер
cd /opt/hysteria-ru
docker compose pull
docker compose up -d
docker compose logs --tail=5

Старые неиспользуемые образы можно удалить:

docker image prune -f

6.4 Мониторинг логов

Hysteria2 пишет структурированные JSON-логи. Полезные паттерны для мониторинга:

# Смотреть все подключения на RU сервере
docker logs hysteria-ru-server 2>&1 | grep -E "connected|disconnected|auth"

# Смотреть ошибки
docker logs hysteria-ru-server 2>&1 | grep -i "error\|fatal\|warn"

# Считать количество уникальных пользователей (поле "id")
docker logs hysteria-ru-server 2>&1 | grep '"id"' | grep -oP '"id": "\K[^"]+' | sort -u

# Следить за логами EU сервера в реальном времени
docker logs -f hysteria-eu 2>&1

Пример нормального лога RU сервера при входящем подключении:

INFO  client connected     {"addr": "CLIENT_IP:PORT", "id": "username"}
INFO  TCP request          {"addr": "CLIENT_IP:PORT", "id": "username", "reqAddr": "example.com:443"}
INFO  client disconnected  {"addr": "CLIENT_IP:PORT", "id": "username", "error": "..."}

Поле "id" — это имя пользователя, возвращённое скриптом check-auth.sh. Это позволяет отслеживать активность конкретных пользователей в логах без хранения паролей.