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. Это позволяет отслеживать активность конкретных пользователей в логах без хранения паролей.