Клиент hysteria2#
Этот документ описывает клиент hysteria2, который есть только в пакете steer-hysteria2. В мета-пакет
steer-extended он не входит: ставится отдельно.
Клиент поднимает TUN-устройство и несёт через него TCP и UDP по протоколу hysteria2 (QUIC). Дальше
устройство ничем не отличается от wg0 для остальной части ядра: метки, таблицы, каналы и failover
работают с ним так же. Строение, общее с клиентом VLESS, — docs/vless.md и раздел
«Туннели» в docs/architecture.md.
Что поддерживается#
- Протокол: hysteria2 — авторизация запросом HTTP/3
POST /auth(ответ233), TCP потоками QUIC, UDP датаграммами QUIC (RFC 9221) с фрагментацией. Эталоны на проводе — ядро hysteria2 (apernet/hysteria) и клиент Xray-core. - Перегрузка: Brutal с заданной скоростью (
upузла) либо BBR (upне задан или сервер просит BBR). Brutal — наш патч к ngtcp2 (build/ngtcp2/patches). - Обфускация: Salamander (
obfs=salamander) и Gecko (obfs=gecko) — пароль не короче 4 знаков. - Проверка сертификата: цепочка до корней роутера и имя;
insecure=1— без проверки;pinSHA256— проверка отпечатка листового сертификата вместо цепочки. - Прыжки по портам: список диапазонов портов сервера и период смены.
ICMP не пересылается: всё, что не TCP и не UDP, получает ICMP «порт недостижим», как у VLESS.
Формат узла#
Ссылка#
hysteria2://пароль@хост:порт/?sni=&insecure=&pinSHA256=&obfs=salamander&obfs-password=&up=&down=&mport=&hop-interval=#имя
Схемы hysteria2:// и hy2:// равнозначны. Всё до @ — пароль целиком (user:pass — тоже пароль,
как в эталоне), с процентным кодированием. Порт по умолчанию — 443; в адресе можно перечислить порты и
диапазоны через запятую (host:443,5000-6000). Адрес IPv6 — в квадратных скобках.
| Ключ | Значение |
|---|---|
sni |
имя для SNI и проверки сертификата; адрес (sni=203.0.113.5, так раздаёт Xray-core узлы с сертификатом на IP) — SNI не уходит, сертификат сверяется с адресом в SAN; без ключа — хост: имя уходит в SNI и сверяется с сертификатом, адрес — без SNI, сверка по SAN IP |
insecure |
1 / true — сертификат не проверяется |
pinSHA256 |
SHA-256 листового сертификата, hex, с двоеточиями или без; проверка цепочки при нём не делается |
obfs, obfs-password |
salamander или gecko и пароль; другие значения obfs — узел пропускается с причиной; размеры Gecko в ссылке не задаются (512..1200) |
up, down |
скорость, Мбит/с (число) либо с единицей (100 mbps, 1 gbps); up включает Brutal, down уходит серверу как желаемая скорость приёма |
mport |
диапазоны портов для прыжков (20000-30000) |
hop-interval |
период смены порта, с (5..3600, по умолчанию 30) |
Ключей up, down, mport и hop-interval в документации эталона нет: их пишут панели и клиенты
подписок, ядро читает их так же.
Конфиг Xray-core#
Подписка может быть конфигом Xray-core: объект с outbounds, массив конфигов или массив outbound.
Читается outbound protocol: hysteria версии 2:
settings.address,settings.port;streamSettings.hysteriaSettings.auth;streamSettings.tlsSettings.serverName,pinnedPeerCertSha256(при списке — первый);streamSettings.finalmask.udp[]: маскаsalamander(settings.password) иudphop(settings.remotePorts,settings.interval);packetSizeу salamander ("600-1100") — это Gecko с такими размерами датаграммы; другая маска — узел пропускается с причиной;streamSettings.finalmask.quicParams.brutalUp,brutalDown.
allowInsecure в конфиге Xray-core снят, поэтому «не проверять сертификат» у конфига выразить нельзя.
Подписка#
Файл подписки — список ссылок по строкам, тот же список в base64 или конфиг Xray-core. Ссылки других
протоколов пропускаются (в foreign); узлы hysteria2, которые клиент не потянет, получают причину
пропуска (hysteria2-nodes, поле skipped_reasons). Скачивает подписку sub-fetch из пакета
steer-vless.
Выход в спеке#
outputs:
hy: { kind: tunnel, protocol: hysteria2, subscription: sub/hy, nodes: [0, 2], over: wg0 }
Ключи те же, что у VLESS: subscription, nodes (номера пригодных узлов; пусто — первый рабочий),
device, over, exclude и exclude_name (какие узлы не брать: страна по флагу в имени, кусок
имени; spec-v2.md), interval и silence (период проверки узла и срок простоя QUIC —
ниже, «Слежка за узлом»). Ключа transport у hysteria2 нет — это ошибка разбора, как и active
больше 1 и by. В v1 — kind: hysteria2,
sub_file, node/nodes. Без пакета steer-hysteria2 спека отвергается словами «kind hysteria2
требует пакет steer-hysteria2».
Подъём и перебор узлов#
steer hysteria2 <выход> читает спеку и подписку, берёт кандидатов (nodes либо все пригодные) и
проверяет их по порядку: рукопожатие QUIC и авторизация, срок — 8 секунд на узел. Первый принявший
авторизацию — узел выхода. Назван один узел номером — проверки при подъёме нет. Затем поднимается
устройство, демону уходит up с именем устройства (маршрут выхода ставит демон), заводится слежка.
Команды: steer hysteria2-nodes <выход|/файл> — узлы подписки JSON-ом (номера, имя, адрес,
обфускация, закрепление, insecure, смена портов, скорость: up_bps больше нуля — Brutal, ноль —
BBR); steer hysteria2-probe <выход|/файл> [--node N] [--timeout С] — проверка узлов с задержкой
рукопожатия и причиной отказа. --insecure (только с файлом, как у vless-nodes) принимается и
ничего не меняет: insecure=1 здесь параметр узла, и такие узлы в перечне всегда.
Соединение и потоки#
Один процесс держит одно QUIC-соединение с узлом. TCP-соединение клиента — поток QUIC: запрос
TCPRequest с адресом назначения, ответ TCPResponse (отказ сервера с сообщением закрывает
клиентское соединение), затем байты в обе стороны. UDP-поток клиента — сессия сервера: датаграммы
UDPMessage с номером сессии и адресом; датаграмма больше предела QUIC режется на фрагменты и
собирается с обеих сторон. Если сервер ответил на авторизацию Hysteria-UDP: false, UDP через узел
не открывается.
Соединение поднимается при старте и держится всё время — и без единого потока: молчащее соединение
каждые 10 секунд шлёт PING (чаще, если срок простоя — свой silence или объявленный сервером —
короче 30 с: PING — не реже трети меньшего из двух сроков). Оборвалось — клиент поднимает его снова сам, не дожидаясь потока: первая
попытка через секунду, дальше пауза удваивается до 30 секунд, пока узел не ответит. Поток, пришедший
при упавшем соединении, тоже поднимает его и ждёт; после неудачной попытки следующая по потоку — не
раньше, чем через секунду.
Обратное давление есть в обе стороны: окно приёма QUIC продлевается по мере того, как байты ушли клиенту, а неотправленное серверу остаётся в потоке.
Перегрузка#
Соединение открывается с Brutal на up узла (или с BBR, если up не задан). Ответ сервера на
авторизацию применяется на ходу, как у ядра hysteria2: сервер ответил auto — BBR; иначе Brutal на
меньшую из скоростей (свой up, предел сервера), причём предел 0 значит «без предела» и остаётся
свой up. (Клиент Xray-core при пределе 0 уходит в BBR; здесь выбрано ядро — сервер без настроек
скорости отвечает именно 0, и up узла иначе не действовал бы.) down узла уходит серверу в
Hysteria-CC-RX.
Salamander, Gecko и прыжки по портам#
Salamander: каждая датаграмма UDP — восемь байт соли и пакет, смешанный с
BLAKE2b-256(пароль ‖ соль). Gecko — тот же Salamander плюс форма: пакеты QUIC с длинным заголовком
(рукопожатие) режутся на 2..8 кусков случайной набивки и размера так, чтобы итоговая датаграмма
попала в [512, 1200] (или в заданные packetSize), короткие идут как есть; принимающая сторона
собирает куски обратно (сборка живёт 8 секунд, одновременных сборок — четыре). Сервер обязан быть
настроен на тот же вид обфускации. Порты для прыжков: отправка идёт на порт из диапазона, меняется не чаще
периода hop-interval при отправке; приём — с любого порта диапазона. Локальный порт сокета не
меняется (у клиента эталона на каждый прыжок открывается новый сокет). Заворачивать диапазон портов
в порт сервера должен сам сервер (nft/iptables).
Слежка за узлом#
Под демоном клиент сам говорит, жив ли узел: раз в interval (ключ выхода, умолчание 60 с) — если
соединение туннеля поднято и узел отвечает на его PING, этого достаточно; иначе проба — рукопожатие и
авторизация отдельным сокетом; после двух неудач подряд — down с причиной, затем круг
проб (свой узел, потом остальные кандидаты); ответил другой — процесс выходит, супервизор поднимает
его заново с тем же перебором. Пула узлов, как у VLESS и прокси, здесь нет: соединение с узлом одно
QUIC на все потоки (hy2conn.c), и выбирать узел на каждое соединение нечем, поэтому active
больше 1 и by у hysteria2 — отказ разбора. silence — срок простоя QUIC: узел, от которого ничего
нет дольше срока, — соединение закрыто, его потоки кончаются, и клиент поднимает соединение заново
(PING — втрое чаще срока, но не реже раза в 10 с; умолчание и silence: 0 — прежние 30 с). Серия отказов открытия потоков зовёт проверку раньше срока (отказ сервера по
конкретному адресу узел не порочит).
status и diag#
В steer status у выхода — nodes и объект hysteria2 (пока клиент жив): node, up, hs_ms
(рукопожатие и авторизация), cc (brutal / bbr), brutal_bps, udp, obfs, hop, rtt_ms,
flows, error. steer diag проверяет, что клиент запущен и соединение поднято.
Ограничения#
- ICMP через узел не идёт.
- Адрес назначения — IPv4 клиента; имён клиент не передаёт (сервер видит адрес).
- Смена узла на ходу невозможна: при потере узла процесс перезапускается целиком.
- Телефонная сборка (профиль android) клиента не содержит.
- Сервер Xray-core от 26.2 до 26.4.25 портит датаграмму UDP, собранную из фрагментов: датаграммы длиннее ~1170 байт (у QUIC это каждый Initial, то есть каждое новое соединение HTTP/3) доходят испорченными, и QUIC через такой узел не открывается — браузер уходит на TCP. Исправлено в Xray 26.5.3; у 26.5.3–26.7.28 ещё теряются датаграммы от 2300 байт. С сервером Xray 26.9.9 и новее и с сервером hysteria2 (apernet) UDP любой длины до 3000 байт проходит. Обновите сервер.