Hysteria 2 каскад

Материал из wolfram
Перейти к навигации Перейти к поиску

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