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

Материал из wolfram
Перейти к навигации Перейти к поиску
мНет описания правки
 
(не показана 1 промежуточная версия этого же участника)
Строка 82: Строка 82:
=== Полная схема прохождения трафика ===
=== Полная схема прохождения трафика ===


[[File:hysteria2-chain-traffic-flow.png|center|900px|Схема трафика Hysteria2: Клиенты → RU Entry Node → EU Exit Node → WARP → Интернет]]
[[File:hysteria2-chain-traffic-flow.png|center|768x768px|Схема трафика Hysteria2: Клиенты → RU Entry Node → EU Exit Node → WARP → Интернет]]


=== Протоколы на каждом участке цепочки ===
=== Протоколы на каждом участке цепочки ===
Строка 875: Строка 875:
<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
# --- ИСХОДНЫЙ КОД СКРИПТА hy2-users.sh ---
#!/bin/bash
# (вставить содержимое файла scripts/hy2-users.sh)
# =============================================================
# 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>

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