splify2
Протоколы

Клиент 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 байт проходит. Обновите сервер.